diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 40332fc3fa1..a7fd0ebbeed 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -68,7 +68,9 @@ Here's how to make a one-off code change. - You can refer to relevant issues in the commit message by writing, e.g., "#105". - Your code should adhere to the `PEP 8 Style Guide`_, with the exception that we have a maximum line length of 99. - + + - Provide static typing with signature annotations. The documentation of `MyPy`_ will be a good start, the cheat sheet is `here`_. + - Document your code. This project uses `sphinx`_ to generate static HTML docs. To build them, first make sure you have the required dependencies: .. code-block:: bash @@ -251,3 +253,5 @@ break the API classes. For example: .. _`Google Python Style Guide`: http://google.github.io/styleguide/pyguide.html .. _`Google Python Style Docstrings`: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html .. _AUTHORS.rst: ../AUTHORS.rst +.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html +.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000000..2f1cd53bcc9 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,39 @@ +name: typing coverage +on: + pull_request: + branches: + - master + - type_hinting_master + paths: + - 'telegram/**' + - '**.py' +jobs: + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies and mypy + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + pip install diff-cover lxml + - name: Verify mypy types + run: | + mypy --version + - name: Running mypy on changed files and save report + run: | + python -m mypy -p telegram --cobertura-xml-report . || true + shell: bash + - name: Computing typing coverage + run: | + diff-cover --fail-under=100 cobertura.xml --html-report typing_coverage.html --compare-branch=origin/$GITHUB_BASE_REF + shell: bash + - name: Uploading coverage report as html + uses: actions/upload-artifact@v1 + with: + name: typing_coverage + path: typing_coverage.html \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85df007add7..515fe78f106 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - master + - type_hinting_master schedule: - cron: 7 3 * * * push: diff --git a/.gitignore b/.gitignore index a98e967bce0..a2e9366ddaf 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ htmlcov/ .coverage.* .cache .pytest_cache +.mypy_cache nosetests.xml coverage.xml *,cover diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46910487dc7..3cf07cf3459 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,24 @@ repos: - repo: git://github.com/python-telegram-bot/mirrors-yapf - sha: 5769e088ef6e0a0d1eb63bd6d0c1fe9f3606d6c8 + rev: 5769e088ef6e0a0d1eb63bd6d0c1fe9f3606d6c8 hooks: - id: yapf files: ^(telegram|tests)/.*\.py$ args: - --diff -- repo: git://github.com/pre-commit/pre-commit-hooks - sha: 0b70e285e369bcb24b57b74929490ea7be9c4b19 +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 hooks: - id: flake8 - repo: git://github.com/pre-commit/mirrors-pylint - sha: 9d8dcbc2b86c796275680f239c1e90dcd50bd398 + rev: v2.4.4 hooks: - id: pylint files: ^telegram/.*\.py$ args: - --errors-only - --disable=import-error +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.761' + hooks: + - id: mypy \ No newline at end of file diff --git a/Makefile b/Makefile index ac90c183a70..c91817264f1 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ PYTEST := pytest PEP257 := pep257 PEP8 := flake8 YAPF := yapf +MYPY := mypy PIP := pip clean: @@ -28,6 +29,9 @@ yapf: lint: $(PYLINT) -E telegram --disable=no-name-in-module,import-error +mypy: + $(MYPY) -p telegram + test: $(PYTEST) -v @@ -41,6 +45,7 @@ help: @echo "- pep8 Check style with flake8" @echo "- lint Check style with pylint" @echo "- yapf Check style with yapf" + @echo "- mypy Check type hinting with mypy" @echo "- test Run tests using pytest" @echo @echo "Available variables:" @@ -49,4 +54,5 @@ help: @echo "- PEP257 default: $(PEP257)" @echo "- PEP8 default: $(PEP8)" @echo "- YAPF default: $(YAPF)" + @echo "- MYPY default: $(MYPY)" @echo "- PIP default: $(PIP)" diff --git a/requirements-dev.txt b/requirements-dev.txt index 577e6dd5381..185f70d9cb7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,7 @@ pep257 pylint flaky yapf +mypy==0.761 pre-commit beautifulsoup4 pytest==4.2.0 diff --git a/setup.cfg b/setup.cfg index e30e2fdacf2..39c8de4322d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,3 +40,8 @@ omit = telegram/__main__.py telegram/vendor/* +[mypy] +warn_unused_configs = True + +[mypy-telegram.vendor.*] +ignore_errors = True \ No newline at end of file diff --git a/telegram/bot.py b/telegram/bot.py index 11794a7ad88..ea743e39d9f 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -18,7 +18,7 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains an object that represents a Telegram Bot.""" +"""This module contains an object that represents a Telegram Bot and is being used as a workflow test now.""" import functools import inspect diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index d7b5dba00a8..d9268783bb8 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -21,7 +21,7 @@ import re from future.utils import string_types - +from typing import Optional from telegram import Chat, Update, MessageEntity __all__ = ['Filters', 'BaseFilter', 'InvertedFilter', 'MergedFilter'] @@ -78,11 +78,11 @@ class variable. (depends on the handler). """ - name = None - update_filter = False - data_filter = False + name: Optional[str] = None + update_filter: bool = False + data_filter: bool = False - def __call__(self, update): + def __call__(self, update: Update) -> Optional[bool]: if self.update_filter: return self.filter(update) else: @@ -97,13 +97,13 @@ def __or__(self, other): def __invert__(self): return InvertedFilter(self) - def __repr__(self): + def __repr__(self) -> str: # We do this here instead of in a __init__ so filter don't have to call __init__ or super() if self.name is None: self.name = self.__class__.__name__ return self.name - def filter(self, update): + def filter(self, update: Update) -> Optional[bool]: """This method must be overwritten. Note: @@ -136,7 +136,7 @@ def __init__(self, f): def filter(self, update): return not bool(self.f(update)) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.f)