Skip to content

Commit c52638c

Browse files
committed
Added mypy testing to check type hints; improved testing for new installs
1 parent be0daef commit c52638c

24 files changed

+298
-186
lines changed

.bumpversion.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.8.1
2+
current_version = 3.8.2a0
33
message = Release {new_version}
44
parse = ^
55
(?P<major>\d+)

.deploy.bat

+9-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ REM 4) run this file!
1212
REM To bump version from x.x.x.rc0 onwards:
1313
REM > bump2version --verbose --allow-dirty --no-commit --no-tag pre --dry-run
1414

15+
REM For manual release in PyCharm:
16+
REM 1) Commit the release; comment is 'Release x.x'
17+
REM 2) In PyCharm Git (ALT+F9) -> Log, right click on the release and select New Tag. Enter tag in format vx.x
18+
REM 3) > git push origin unreleased:main --tags
19+
1520
set "project=%~dp0"
1621
set "project=%project:~0,-1%"
1722
set "project=%project:\=" & set "project=%"
@@ -51,21 +56,14 @@ if %r% EQU N r=n
5156
if %r% EQU n exit /b
5257

5358
echo.
54-
echo Bumping version to next %v%
59+
echo Bumping version to next %v%; this will also commit the changes.
5560
echo.
5661
bump2version --commit --tag --no-sign-tags %v%
57-
if NOT ["%errorlevel%"]==["0"] (
58-
echo.
59-
echo.
60-
echo errorlevel: "%errorlevel%"
61-
echo make sure to commit all files (other than those that will be modified by bump2version)
62-
echo before rerunning this script
63-
pause
64-
exit /b %errorlevel%
65-
)
62+
63+
pause
6664

6765
echo.
68-
echo Pushing branch to main and setting release tag
66+
echo Pushing branch to main including release tag
6967
git push origin unreleased:main --tags
7068
if NOT ["%errorlevel%"]==["0"] (
7169
pause

.pre-commit-config.yaml

+15-14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313

1414
# NOTE: in this package tox runs 'pre-commit run -a'
1515

16+
# Force all unspecified python hooks to run python3
17+
default_language_version:
18+
python: python3
19+
20+
1621
# A list of repository mappings.
1722
repos:
1823
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -39,8 +44,8 @@ repos:
3944
# description: Checks that scripts with shebangs are executable
4045
# - id: check-symlinks
4146
# description: Checks for symlinks which do not point to anything
42-
# - id: check-toml
43-
# description: Attempts to load all TOML files to verify syntax.
47+
- id: check-toml
48+
description: Attempts to load all TOML files to verify syntax.
4449
- id: check-vcs-permalinks
4550
description: Ensures that links to vcs websites are permalinks
4651
# - id: check-xml
@@ -123,8 +128,7 @@ repos:
123128
name: Find common security issues in Python code (bandit)
124129
entry: bandit
125130
language: python
126-
language_version: python3
127-
types: [ python ]
131+
types: [python]
128132
args: [--ini, .bandit, --recursive]
129133
# isort not needed if running black
130134
# - id: isort # https://github.com/PyCQA/isort
@@ -139,37 +143,34 @@ repos:
139143
name: Convert % strings to f-strings (flynt)
140144
entry: flynt
141145
language: python
142-
language_version: python3
143146
require_serial: true
144147
types: [python]
145148
args: [--fail-on-change]
146149
- id: black # https://github.com/python/black
147150
name: Uncompromising code formatting (black)
148151
entry: black
149152
language: python
150-
language_version: python3
151153
types: [python]
152154
- id: flake8
153155
name: Check for errors, style, [and over-complexity] (flake8)
154156
description: Code linter
155157
entry: flake8 # https://gitlab.com/pycqa/flake8
156158
language: python
157-
language_version: python3
158159
types: [python]
159160
require_serial: true
160161
args: [--max-line-length, '120']
161-
# - id: mypy # https://github.com/python/mypy
162-
# name: Static Typing for Python (mypy)
163-
# entry: mypy
164-
# language: python
165-
# language_version: python3
166-
# types: [python]
162+
- id: mypy # https://github.com/python/mypy
163+
name: Static Typing for Python (mypy)
164+
entry: mypy
165+
language: python
166+
types: [python]
167+
pass_filenames: false
168+
args: [-p, webchanges]
167169
- id: doc8 # https://github.com/pycqa/doc8
168170
name: Check for style in rst documentation (doc8)
169171
description: Documentation linter
170172
entry: doc8
171173
language: python
172-
language_version: python3
173174
types: [rst]
174175
require_serial: true
175176
args: [--max-line-length, '120']

CHANGELOG.rst

+9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ can check out the `wish list <https://github.com/mborsetti/webchanges/blob/main/
3131
Internals, for changes that don't affect users. [triggers a minor patch]
3232
3333
34+
Version 3.8.2a0
35+
====================
36+
Unreleased
37+
38+
Internals
39+
---------
40+
* Added mypy testing for type hint checks
41+
42+
3443
Version 3.8.1
3544
====================
3645
2021-08-03

docs/cli.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ Test run a job
9494
You can test a job and its filter by using the argument ``--test`` followed by the job index number (from ``--list``) or
9595
its URL/command; :program:`webchanges` will display the filtered output. This allows to easily test changes in
9696
filters. Use a negative index number to select a job from the bottom of your job list (i.e. -1 is the last job, -2 is
97-
the second to last job, etc.)
97+
the second to last job, etc.). Combine ``--test`` with ``--verbose`` to get more information, for example the text
98+
returned from a website with a 4xx (client error) status code::
99+
100+
webchanges --verbose --test 1
98101

99102
.. versionchanged:: 3.8
100103
Accepts negative indices.

docs/filters.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,12 @@ but may not yield the prettiest of results.
351351
352352
``bs4``
353353
^^^^^^^
354-
This filter method extracts unformatted text from HTML using the `Beautiful Soup
355-
<https://pypi.org/project/beautifulsoup4/>`__, specifically its `get_text(strip=True)
354+
This filter method extracts human-readable text from HTML using the `Beautiful Soup
355+
<https://pypi.org/project/beautifulsoup4/>`__ Python package, specifically its `get_text(strip=True)
356356
<https://www.crummy.com/software/BeautifulSoup/bs4/doc/#get-text>`__ method.
357357

358358
.. note:: As of Beautiful Soup version 4.9.0, when using the ``lxml`` or ``html.parser`` parser (see optional
359-
sub-directive below), the contents of <script>, <style>, and <template> tags are not considered to be ‘text’, since
359+
sub-directive below), the contents of <script>, <style>, and <template> tags are not considered to be ‘text’ since
360360
those tags are not part of the human-visible content of the page.
361361

362362
.. code-block:: yaml

mypy.ini

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[mypy]
2+
# Static Typing for Python
3+
# Runs as part of pre-commit
4+
# File info at https://mypy.readthedocs.io/en/stable/config_file.html
5+
6+
# Specifies the Python version used to parse and check the target program.
7+
python_version = 3.7
8+
9+
# Shows error codes in error messages.
10+
show_error_codes = True
11+
12+
# Suppresses error messages about imports that cannot be resolved.
13+
ignore_missing_imports = True
14+
15+
# Disallows calling functions without type annotations from functions with type annotations.
16+
disallow_untyped_calls = True
17+
18+
# Disallows defining functions without type annotations or with incomplete type annotations.
19+
disallow_untyped_defs = True
20+
21+
# Reports an error whenever a function with type annotations is decorated with a decorator without annotations.
22+
disallow_untyped_decorators = True
23+
24+
# Changes the treatment of arguments with a default value of None by not implicitly making their type Optional.
25+
no_implicit_optional = True
26+
27+
# Warns about casting an expression to its inferred type.
28+
warn_redundant_casts = True
29+
30+
# Warns about unneeded # type: ignore comments.
31+
warn_unused_ignores = True
32+
33+
# Shows a warning when returning a value with type Any from a function declared with a non-Any return type.
34+
# warn_return_any = True
35+
36+
# Shows a warning when encountering any code inferred to be unreachable or redundant after performing type analysis.
37+
warn_unreachable = False
38+
39+
# Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location
40+
# markers.
41+
pretty = True
42+
43+
# Use an SQLite database to store the cache.
44+
sqlite_cache = True
45+
46+
# Warns about per-module sections in the config file that do not match any files processed when invoking mypy.
47+
warn_unused_configs = True

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[build-system]
2+
# Minimum requirements for the build system to execute.
3+
requires = ['setuptools', 'wheel'] # PEP 508 specifications.
4+
15
[tool.black]
26
# Uncompromising code formatting
37
# Runs as part of pre-commit

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
'license': project.__license__,
5959
# Below to include in sdist the files read above (see https://stackoverflow.com/questions/37753833)
6060
# data_files is deprecated. It does not work with wheels, so it should be avoided.
61+
'zip_safe': True,
6162
'install_requires': list(requirements),
6263
'entry_points': {'console_scripts': [f'{project.__project_name__}={project.__package__}.cli:main']},
6364
'extras_require': {
@@ -81,6 +82,7 @@
8182
'CI': f'{project.__code_url__}actions',
8283
'Documentation': project.__docs_url__,
8384
'Source Code': project.__code_url__,
85+
'Changelog': f'{project.__docs_url__}changelog.html',
8486
},
8587
}
8688
SETUP['extras_require']['all'] = sorted(list(set(pkg for extra in SETUP['extras_require'].values() for pkg in extra)))

tests/command_test.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_edit_hooks(capsys):
114114
with pytest.raises(SystemExit) as pytest_wrapped_e:
115115
urlwatch_command.handle_actions()
116116
setattr(command_config, 'edit_hooks', False)
117-
assert pytest_wrapped_e.value.code is None
117+
assert pytest_wrapped_e.value.code == 0
118118
message = capsys.readouterr().out
119119
assert message == f'Saved edits in {urlwatch_command.urlwatch_config.hooks}\n'
120120

@@ -147,7 +147,7 @@ def test_show_features_and_verbose(capsys):
147147
urlwatch_command.handle_actions()
148148
setattr(command_config, 'features', False)
149149
setattr(command_config, 'verbose', False)
150-
assert pytest_wrapped_e.value.code is None
150+
assert pytest_wrapped_e.value.code == 0
151151
message = capsys.readouterr().out
152152
assert '* browser - Retrieve a URL, emulating a real web browser (use_browser: true).' in message
153153

@@ -274,7 +274,7 @@ def test_test_diff_and_joblist(capsys):
274274
with pytest.raises(SystemExit) as pytest_wrapped_e:
275275
urlwatch_command.handle_actions()
276276
setattr(command_config, 'test_diff', None)
277-
assert pytest_wrapped_e.value.code is None
277+
assert pytest_wrapped_e.value.code == 0
278278
message = capsys.readouterr().out
279279
assert '=== Filtered diff between state 0 and state -1 ===\n' in message
280280
# rerun to reuse cached _generated_diff
@@ -296,7 +296,7 @@ def test_test_diff_and_joblist(capsys):
296296
with pytest.raises(SystemExit) as pytest_wrapped_e:
297297
urlwatch_command.handle_actions()
298298
setattr(command_config, 'test_diff', None)
299-
assert pytest_wrapped_e.value.code is None
299+
assert pytest_wrapped_e.value.code == 0
300300
message = capsys.readouterr().out
301301
assert '=== Filtered diff between state 0 and state -1 ===\n' in message
302302
finally:
@@ -470,7 +470,7 @@ def test_check_edit_config():
470470
with pytest.raises(SystemExit) as pytest_wrapped_e:
471471
urlwatch_command.check_edit_config()
472472
setattr(command_config, 'edit_config', False)
473-
assert pytest_wrapped_e.value.code is None
473+
assert pytest_wrapped_e.value.code == 0
474474

475475

476476
def test_check_edit_config_fail(capsys):

tests/jobs_test.py

-1
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,6 @@ def test_browser_block_elements_not_str_or_list():
388388
job = JobBase.unserialize(job_data)
389389
job_state = JobState(cache_storage, job)
390390
job_state.process()
391-
print('job_state.exception', job_state.exception)
392391
assert isinstance(job_state.exception, TypeError)
393392

394393

tests/storage_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_clean_cache_no_clean_all():
218218
assert timestamps[1] < timestamps[0]
219219

220220
# clean cache without using clean_all
221-
delattr(CacheSQLite3Storage, 'clean_all')
221+
# delattr(CacheSQLite3Storage, 'clean_all')
222222
cache_storage.clean_cache([guid])
223223
history = cache_storage.get_history_data(guid)
224224
assert len(history) == 1

tox.ini

+10-5
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
minversion = 3.23.1
1010
envlist =
1111
pre-commit,
12-
new-install,
1312
py{310,39,38,37},
1413
post,
15-
docs
14+
docs,
15+
new-install,
1616
skip_missing_interpreters = true
1717

1818
[testenv]
@@ -24,7 +24,7 @@ setenv =
2424
PYTHONUTF8 = 1
2525
# parallel_show_output = true
2626
depends =
27-
py{310,39,38,37}: pre-commit, new-install
27+
# py{310,39,38,37}: pre-commit, new-install
2828
post: py{310,39,38,37}
2929

3030
[testenv:pre-commit]
@@ -40,19 +40,24 @@ commands =
4040

4141
[testenv:new-install]
4242
# Settings defined in the top-level testenv section are automatically inherited if not overwritten
43-
# new-install tests a clean new installation, ensuring e.g. that all packages are installed as well
43+
# new-install tests a clean new installation using wheel, ensuring e.g. that all packages are installed as well
4444
basepython = python3.7
4545
commands =
46+
python setup.py bdist_wheel
47+
pip install --upgrade --find-links={toxinidir}/dist webchanges
4648
webchanges -v --clean-cache
4749
python -c "from pathlib import Path; dir = Path.home().joinpath('Documents').joinpath('webchanges'); [f.unlink() for f in dir.iterdir()]; dir.rmdir()"
50+
deps = wheel
4851
setenv = USERPROFILE = {env:TEMP}
52+
download = true
53+
skip_install = true
4954
isolated_build = true
5055

5156
[testenv:post]
5257
# Post-test cleanup
5358
commands =
5459
coverage combine
55-
coverage html --fail-under=74
60+
coverage html --fail-under=78
5661
# TODO The below works in Windows only
5762
cmd /c start "" htmlcov/index.html
5863
deps =

webchanges/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
# * MINOR version when you add functionality in a backwards compatible manner, and
1717
# * MICRO or PATCH version when you make backwards compatible bug fixes. We no longer use '0'
1818
# If unsure on increments, use pkg_resources.parse_version to parse
19-
__version__ = '3.8.1'
19+
__version__ = '3.8.2a0'
2020
__description__ = 'Check web (or commands) for changes since last run and notify'
2121
__author__ = 'Mike Borsetti <[email protected]>'
2222
__copyright__ = 'Copyright 2020- Mike Borsetti'
2323
__license__ = 'MIT, BSD 3-Clause License'
2424
__url__ = f'https://pypi.org/project/{__project_name__}/'
2525
__code_url__ = f'https://www.github.com/mborsetti/{__project_name__}/'
26-
__docs_url__ = f'https://{__project_name__}.readthedocs.io'
26+
__docs_url__ = f'https://{__project_name__}.readthedocs.io/'
2727
__user_agent__ = f'{__project_name__}/{__version__} (+{__url__})' # TODO find out why url is prepended by '+'
2828

2929
from typing import Dict, Union

webchanges/cli.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,19 @@
2828
YamlJobsStorage,
2929
)
3030

31-
# directory where the config, jobs and hooks files are located
31+
# Directory where the config, jobs and hooks files are located
3232
if os.name != 'nt':
33-
# typically ~/.config/{__project_name__}
34-
config_path = user_config_path(__project_name__)
33+
config_path = user_config_path(__project_name__) # typically ~/.config/{__project_name__}
3534
else:
3635
config_path = Path.home().joinpath('Documents').joinpath(__project_name__)
3736

3837
# directory where the database is located
3938
# typically ~/.cache/{__project_name__} or %LOCALAPPDATA%\{__project_name__}\{__project_name__}\Cache
4039
cache_path = user_cache_path(__project_name__)
4140

42-
# Ignore SIGPIPE for stdout (see https://github.com/thp/urlwatch/issues/77)
43-
try:
41+
# Ignore signal SIGPIPE ("broken pipe") for stdout (see https://github.com/thp/urlwatch/issues/77)
42+
if os.name != 'nt': # Windows does not have signal.SIGPIPE
4443
signal.signal(signal.SIGPIPE, signal.SIG_DFL) # type: ignore[attr-defined] # not defined in Windows
45-
except AttributeError:
46-
# Windows does not have signal.SIGPIPE
47-
pass
4844

4945
logger = logging.getLogger(__name__)
5046

0 commit comments

Comments
 (0)