11"""Development tasks."""
22
3+ import importlib
34import os
45import re
56import sys
7+ import tempfile
8+ from contextlib import suppress
69from functools import wraps
10+ from io import StringIO
711from pathlib import Path
8- from shutil import which
912from typing import List , Optional , Pattern
1013from urllib .request import urlopen
1114
@@ -105,7 +108,7 @@ def changelog(ctx):
105108 )
106109
107110
108- @duty (pre = ["check_code_quality " , "check_types" , "check_docs" , "check_dependencies" ])
111+ @duty (pre = ["check_quality " , "check_types" , "check_docs" , "check_dependencies" ])
109112def check (ctx ):
110113 """
111114 Check it all!
@@ -116,7 +119,7 @@ def check(ctx):
116119
117120
118121@duty
119- def check_code_quality (ctx , files = PY_SRC ):
122+ def check_quality (ctx , files = PY_SRC ):
120123 """
121124 Check the code quality.
122125
@@ -141,22 +144,36 @@ def check_dependencies(ctx):
141144 Arguments:
142145 ctx: The context instance (passed automatically).
143146 """
144- nofail = False
145- safety = which ("safety" )
146- if not safety :
147- pipx = which ("pipx" )
148- if pipx :
149- safety = f"{ pipx } run safety"
150- else :
151- safety = "safety"
152- nofail = True
153- ctx .run (
154- f"pdm export -f requirements --without-hashes | { safety } check --stdin --full-report" ,
155- title = "Checking dependencies" ,
156- pty = PTY ,
157- nofail = nofail ,
147+ # undo possible patching
148+ # see https://github.com/pyupio/safety/issues/348
149+ for module in sys .modules : # noqa: WPS528
150+ if module .startswith ("safety." ) or module == "safety" :
151+ del sys .modules [module ] # noqa: WPS420
152+
153+ importlib .invalidate_caches ()
154+
155+ # reload original, unpatched safety
156+ from safety .formatter import report
157+ from safety .safety import check as safety_check
158+ from safety .util import read_requirements
159+
160+ # retrieve the list of dependencies
161+ requirements = ctx .run (
162+ ["pdm" , "export" , "-f" , "requirements" , "--without-hashes" ],
163+ title = "Exporting dependencies as requirements" ,
164+ allow_overrides = False ,
158165 )
159166
167+ # check using safety as a library
168+ def safety (): # noqa: WPS430
169+ packages = list (read_requirements (StringIO (requirements )))
170+ vulns = safety_check (packages = packages , ignore_ids = "" , key = "" , db_mirror = "" , cached = False , proxy = {})
171+ output_report = report (vulns = vulns , full = True , checked_packages = len (packages ))
172+ if vulns :
173+ print (output_report )
174+
175+ ctx .run (safety , title = "Checking dependencies" )
176+
160177
161178def no_docs_py36 (nofail = True ):
162179 """
@@ -197,15 +214,57 @@ def check_docs(ctx, strict: bool = False):
197214 ctx .run (f"mkdocs build{ ' -s' if strict else '' } " , title = "Building documentation" , pty = PTY )
198215
199216
200- @duty
201- def check_types (ctx ):
217+ @duty # noqa: WPS231
218+ def check_types (ctx ): # noqa: WPS231
202219 """
203220 Check that the code is correctly typed.
204221
205222 Arguments:
206223 ctx: The context instance (passed automatically).
207224 """
208- ctx .run (f"mypy --config-file config/mypy.ini { PY_SRC } " , title = "Type-checking" , pty = PTY )
225+ # NOTE: the following code works around this issue:
226+ # https://github.com/python/mypy/issues/10633
227+
228+ # compute packages directory path
229+ py = f"{ sys .version_info .major } .{ sys .version_info .minor } "
230+ pkgs_dir = Path ("__pypackages__" , py , "lib" ).resolve ()
231+
232+ # build the list of available packages
233+ packages = {}
234+ for package in pkgs_dir .glob ("*" ):
235+ if package .suffix not in {".dist-info" , ".pth" } and package .name != "__pycache__" :
236+ packages [package .name ] = package
237+
238+ # handle .pth files
239+ for pth in pkgs_dir .glob ("*.pth" ):
240+ with suppress (OSError ):
241+ for package in Path (pth .read_text ().splitlines ()[0 ]).glob ("*" ): # noqa: WPS440
242+ if package .suffix != ".dist-info" :
243+ packages [package .name ] = package
244+
245+ # create a temporary directory to assign to MYPYPATH
246+ with tempfile .TemporaryDirectory () as tmpdir :
247+
248+ # symlink the stubs
249+ ignore = set ()
250+ for stubs in (path for name , path in packages .items () if name .endswith ("-stubs" )): # noqa: WPS335
251+ Path (tmpdir , stubs .name ).symlink_to (stubs , target_is_directory = True )
252+ # try to symlink the corresponding package
253+ # see https://www.python.org/dev/peps/pep-0561/#stub-only-packages
254+ pkg_name = stubs .name .replace ("-stubs" , "" )
255+ if pkg_name in packages :
256+ ignore .add (pkg_name )
257+ Path (tmpdir , pkg_name ).symlink_to (packages [pkg_name ], target_is_directory = True )
258+
259+ # create temporary mypy config to ignore stubbed packages
260+ newconfig = Path ("config" , "mypy.ini" ).read_text ()
261+ newconfig += "\n " + "\n \n " .join (f"[mypy-{ pkg } .*]\n ignore_errors=true" for pkg in ignore )
262+ tmpconfig = Path (tmpdir , "mypy.ini" )
263+ tmpconfig .write_text (newconfig )
264+
265+ # set MYPYPATH and run mypy
266+ os .environ ["MYPYPATH" ] = tmpdir
267+ ctx .run (f"mypy --config-file { tmpconfig } { PY_SRC } " , title = "Type-checking" , pty = PTY )
209268
210269
211270@duty (silent = True )
@@ -301,7 +360,7 @@ def release(ctx, version):
301360 ctx .run ("git push --tags" , title = "Pushing tags" , pty = False )
302361 ctx .run ("pdm build" , title = "Building dist/wheel" , pty = PTY )
303362 ctx .run ("twine upload --skip-existing dist/*" , title = "Publishing version" , pty = PTY )
304- docs_deploy .run () # type: ignore
363+ docs_deploy .run ()
305364
306365
307366@duty (silent = True )
0 commit comments