diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 0000000..e4f5a6d
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+
+ # Check for updates to GitHub Actions every month
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
index 07b72df..024f40f 100644
--- a/.github/workflows/check.yaml
+++ b/.github/workflows/check.yaml
@@ -17,35 +17,37 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.x'
+ - name: Install dependencies
+ run: pip install tox
- name: Run Tests
- run: python setup.py pytest
+ run: tox -e py3
code-ql:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: python
queries: security-and-quality
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
doc8-lint:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Run reStructuredText Linter
- uses: deep-entertainment/doc8-action@v4
+ uses: deep-entertainment/doc8-action@v5
with:
scanPaths: ${{ github.workspace }}
@@ -54,11 +56,11 @@ jobs:
fail-fast: false
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Run flake8 Linter
run: |
- pip install flake8
+ pip install -e . flake8
flake8 --count --show-source --statistics src test
diff --git a/.github/workflows/codecov.yaml b/.github/workflows/codecov.yaml
index aaaabbe..4db7d2a 100644
--- a/.github/workflows/codecov.yaml
+++ b/.github/workflows/codecov.yaml
@@ -8,13 +8,17 @@ jobs:
if: github.repository_owner == 'arkq'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Generate Coverage Report
run: |
- pip install coverage
- coverage run --include=src/* setup.py pytest
+ pip install tox
+ tox
- name: Upload Coverage to Codecov
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: .tox/coverage.xml
+ disable_search: true
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index cc3192b..8ce1ea6 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -7,19 +7,19 @@ jobs:
deploy:
if: github.repository_owner == 'arkq'
runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/flake8-requirements
+ permissions:
+ id-token: write
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install setuptools wheel twine
- - name: Build and publish
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: |
- python setup.py bdist_wheel
- twine upload dist/*
+ run: pip install build
+ - name: Build
+ run: python -m build
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/README.rst b/README.rst
index 26a53e8..61e21a1 100644
--- a/README.rst
+++ b/README.rst
@@ -7,7 +7,7 @@ package import requirements. It reports missing and/or not used project direct d
This plug-in adds new flake8 warnings:
- ``I900``: Package is not listed as a requirement.
-- ``I901``: Package is required but not used.
+- ``I901``: Package is required but not used. (not implemented yet)
Important notice
----------------
@@ -64,8 +64,9 @@ Real life example::
max-line-length = 100
known-modules = my-lib:[mylib.drm,mylib.encryption]
-If you use `flake8-pyproject `_, you can also configure
-the known modules using a nicer syntax::
+If you use `Flake8-pyproject `_
+(can include for installation using ``flake8-requirements[pyproject]``),
+you can also configure the known modules using a nicer syntax in ``pyproject.toml``::
$ cat pyproject.toml
...
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a80295e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,42 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel", "build"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "flake8-requirements"
+# NOTE: Keep in sync with src/flake8_requirements/checker.py file.
+version = "2.2.1"
+description = "Package requirements checker, plugin for flake8"
+readme = "README.rst"
+authors = [ { name = "Arkadiusz Bokowy", email = "arkadiusz.bokowy@gmail.com" } ]
+requires-python = ">=3.6"
+classifiers = [
+ "Framework :: Flake8",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Software Development :: Quality Assurance",
+]
+dependencies = [
+ "flake8 >= 4.0.0",
+ "setuptools >= 10.0.0",
+ "tomli>=1.2.1; python_version < '3.11'",
+]
+
+[project.optional-dependencies]
+pyproject = ["Flake8-pyproject"]
+
+[project.urls]
+Homepage = "https://github.com/arkq/flake8-requirements"
+
+[project.entry-points."flake8.extension"]
+I90 = "flake8_requirements:Flake8Checker"
+
+[tool.doc8]
+max-line-length = 99
+
+[tool.isort]
+force_single_line = true
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index e096a8c..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[bdist_wheel]
-universal = 1
-
-[doc8]
-max-line-length = 99
-
-[isort]
-force_single_line = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 22ec537..0000000
--- a/setup.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import with_statement
-
-import re
-from os import path
-
-from setuptools import setup
-
-
-def get_abs_path(pathname):
- return path.join(path.dirname(__file__), pathname)
-
-
-with open(get_abs_path("src/flake8_requirements/checker.py")) as f:
- version = re.match(r'.*__version__ = "(.*?)"', f.read(), re.S).group(1)
-with open(get_abs_path("README.rst")) as f:
- long_description = f.read()
-
-setup(
- name="flake8-requirements",
- version=version,
- author="Arkadiusz Bokowy",
- author_email="arkadiusz.bokowy@gmail.com",
- url="https://github.com/Arkq/flake8-requirements",
- description="Package requirements checker, plugin for flake8",
- long_description=long_description,
- license="MIT",
- package_dir={'': "src"},
- packages=["flake8_requirements"],
- install_requires=[
- "flake8 >= 2.0.0",
- "setuptools >= 10.0.0",
- "toml >= 0.7.0",
- ],
- setup_requires=["pytest-runner"],
- tests_require=["mock", "pytest"],
- entry_points={
- 'flake8.extension': [
- 'I90 = flake8_requirements:Flake8Checker',
- ],
- },
- classifiers=[
- "Framework :: Flake8",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 3",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Software Development :: Quality Assurance",
- ],
-)
diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py
index d16aa93..1c3ad93 100644
--- a/src/flake8_requirements/checker.py
+++ b/src/flake8_requirements/checker.py
@@ -8,17 +8,19 @@
from functools import wraps
from logging import getLogger
-import flake8
-import toml
from pkg_resources import parse_requirements
from pkg_resources import yield_lines
+if sys.version_info >= (3, 11):
+ import tomllib
+else:
+ import tomli as tomllib
+
from .modules import KNOWN_3RD_PARTIES
-from .modules import STDLIB_PY2
from .modules import STDLIB_PY3
-# NOTE: Changing this number will alter package version as well.
-__version__ = "1.7.5"
+# NOTE: Keep in sync with pyproject.toml file.
+__version__ = "2.2.1"
__license__ = "MIT"
LOG = getLogger('flake8.plugin.requirements')
@@ -29,10 +31,7 @@
}
STDLIB = set()
-if sys.version_info[0] == 2:
- STDLIB.update(STDLIB_PY2)
-if sys.version_info[0] == 3:
- STDLIB.update(STDLIB_PY3)
+STDLIB.update(STDLIB_PY3)
def memoize(f):
@@ -55,14 +54,14 @@ def modsplit(module):
return tuple(module.split("."))
-def project2module(project):
- """Convert project name into a module name."""
+def project2modules(project):
+ """Convert project name into auto-detected module names."""
# Name unification in accordance with PEP 426.
- project = project.lower().replace("-", "_")
- if project.startswith("python_"):
+ modules = [project.lower().replace("-", "_")]
+ if modules[0].startswith("python_"):
# Remove conventional "python-" prefix.
- project = project[7:]
- return project
+ modules.append(modules[0][7:])
+ return modules
def joinlines(lines):
@@ -298,8 +297,9 @@ class Flake8Checker(object):
# Build-in mapping for known 3rd party modules.
known_3rd_parties = {
- project2module(k): v
+ k: v
for k, v in KNOWN_3RD_PARTIES.items()
+ for k in project2modules(k)
}
# Host-based mapping for 3rd party modules.
@@ -329,48 +329,46 @@ def __init__(self, tree, filename, lines=None):
@classmethod
def add_options(cls, manager):
"""Register plug-in specific options."""
- kw = {}
- if flake8.__version__ >= '3.0.0':
- kw['parse_from_config'] = True
manager.add_option(
"--known-modules",
action='store',
default="",
+ parse_from_config=True,
help=(
"User defined mapping between a project name and a list of"
" provided modules. For example: ``--known-modules=project:"
"[Project],extra-project:[extras,utilities]``."
- ),
- **kw)
+ ))
manager.add_option(
"--requirements-file",
action='store',
+ parse_from_config=True,
help=(
"Specify the name (location) of the requirements text file. "
"Unless an absolute path is given, the file will be searched "
"relative to the project's root directory. If this option is "
- "given, requirements from setup.py, setup.cfg or "
- "pyproject.toml will not be taken into account."
- ),
- **kw)
+ "not specified, the plugin look up for requirements in "
+ "(1) setup.py, (2) setup.cfg, (3) pyproject.toml, and (4) "
+ "requirements.txt. If specified, look up will not take place."
+ ))
manager.add_option(
"--requirements-max-depth",
- type=int if flake8.__version__ >= '3.8.0' else 'int',
+ type=int,
default=1,
+ parse_from_config=True,
help=(
"Max depth to resolve recursive requirements. Defaults to 1 "
"(one level of recursion allowed)."
- ),
- **kw)
+ ))
manager.add_option(
"--scan-host-site-packages",
action='store_true',
+ parse_from_config=True,
help=(
"Scan host's site-packages directory for 3rd party projects, "
"which provide more than one module or the name of the module"
" is different than the project name itself."
- ),
- **kw)
+ ))
@classmethod
def parse_options(cls, options):
@@ -378,15 +376,17 @@ def parse_options(cls, options):
if isinstance(options.known_modules, dict):
# Support for nicer known-modules using flake8-pyproject.
cls.known_modules = {
- project2module(k): v for k, v in options.known_modules.items()
+ k: v
+ for k, v in options.known_modules.items()
+ for k in project2modules(k)
}
else:
cls.known_modules = {
- project2module(k): v.split(",")
+ k: v.split(",")
for k, v in [
x.split(":[")
- for x in re.split(r"],?", options.known_modules)[:-1]
- ]
+ for x in re.split(r"],?", options.known_modules)[:-1]]
+ for k in project2modules(k)
}
cls.requirements_file = options.requirements_file
cls.requirements_max_depth = options.requirements_max_depth
@@ -422,7 +422,8 @@ def discover_host_3rd_party_modules():
), "")
with open(modules_path) as f:
modules = list(yield_lines(f.readlines()))
- mapping[project2module(name)] = modules
+ for name in project2modules(name):
+ mapping[name] = modules
return mapping
@staticmethod
@@ -545,11 +546,12 @@ def resolve_requirement(cls, requirement, max_depth=0, path=None):
@memoize
def get_pyproject_toml(cls):
"""Try to load PEP 518 configuration file."""
+ pyproject_config_path = os.path.join(cls.root_dir, "pyproject.toml")
try:
- with open(os.path.join(cls.root_dir, "pyproject.toml")) as f:
- return toml.loads(f.read())
- except (IOError, toml.TomlDecodeError) as e:
- LOG.debug("Couldn't load project setup: %s", e)
+ with open(pyproject_config_path, mode="rb") as f:
+ return tomllib.load(f)
+ except (IOError, tomllib.TOMLDecodeError) as e:
+ LOG.debug("Couldn't load pyproject: %s", e)
return {}
@classmethod
@@ -558,6 +560,33 @@ def get_pyproject_toml_pep621(cls):
cfg_pep518 = cls.get_pyproject_toml()
return cfg_pep518.get('project', {})
+ @classmethod
+ def get_setuptools_dynamic_requirements(cls):
+ """Retrieve dynamic requirements defined in setuptools config."""
+ cfg = cls.get_pyproject_toml()
+ dynamic_keys = cfg.get('project', {}).get('dynamic', [])
+ dynamic_config = (
+ cfg.get('tool', {}).get('setuptools', {}).get('dynamic', {})
+ )
+ requirements = []
+ files_to_parse = []
+ if 'dependencies' in dynamic_keys:
+ files_to_parse.extend(
+ dynamic_config.get('dependencies', {}).get('file', [])
+ )
+ if 'optional-dependencies' in dynamic_keys:
+ for element in dynamic_config.get(
+ 'optional-dependencies', {}
+ ).values():
+ files_to_parse.extend(element.get('file', []))
+ for file_path in files_to_parse:
+ try:
+ with open(file_path, 'r') as file:
+ requirements.extend(parse_requirements(file))
+ except IOError as e:
+ LOG.debug("Couldn't open requirements file: %s", e)
+ return requirements
+
@classmethod
def get_pyproject_toml_pep621_requirements(cls):
"""Try to get PEP 621 metadata requirements."""
@@ -567,6 +596,8 @@ def get_pyproject_toml_pep621_requirements(cls):
pep621.get("dependencies", ())))
for r in pep621.get("optional-dependencies", {}).values():
requirements.extend(parse_requirements(r))
+ if len(requirements) == 0:
+ requirements = cls.get_setuptools_dynamic_requirements()
return requirements
@classmethod
@@ -644,7 +675,7 @@ def get_setup_py(cls):
with open(os.path.join(cls.root_dir, "setup.py")) as f:
return SetupVisitor(ast.parse(f.read()), cls.root_dir)
except IOError as e:
- LOG.debug("Couldn't load project setup: %s", e)
+ LOG.debug("Couldn't load setup: %s", e)
return SetupVisitor(ast.parse(""), cls.root_dir)
@classmethod
@@ -663,12 +694,15 @@ def get_setup_py_requirements(cls, is_setup_py):
def get_mods_1st_party(cls):
mods_1st_party = ModuleSet()
# Get 1st party modules (used for absolute imports).
- modules = [project2module(
+ modules = project2modules(
cls.get_setup_py().keywords.get('name') or
cls.get_setup_cfg().get('metadata', 'name') or
cls.get_pyproject_toml_pep621().get('name') or
cls.get_pyproject_toml_poetry().get('name') or
- "")]
+ "")
+ # Use known module mappings to correct auto-detected name. Please note
+ # that we're using the first module name only, since all mappings shall
+ # contain all possible auto-detected module names.
if modules[0] in cls.known_modules:
modules = cls.known_modules[modules[0]]
for module in modules:
@@ -681,7 +715,8 @@ def get_mods_3rd_party(cls, is_setup_py):
mods_3rd_party = ModuleSet()
# Get 3rd party module names based on requirements.
for requirement in cls.get_mods_3rd_party_requirements(is_setup_py):
- modules = [project2module(requirement.project_name)]
+ modules = project2modules(requirement.project_name)
+ # Use known module mappings to correct auto-detected module name.
if modules[0] in cls.known_modules:
modules = cls.known_modules[modules[0]]
elif modules[0] in cls.known_3rd_parties:
diff --git a/src/flake8_requirements/modules.py b/src/flake8_requirements/modules.py
index c390c35..f63eebb 100644
--- a/src/flake8_requirements/modules.py
+++ b/src/flake8_requirements/modules.py
@@ -1,285 +1,3 @@
-# List of all modules (standard library) available in Python 2.
-STDLIB_PY2 = (
- "AL",
- "BaseHTTPServer",
- "Bastion",
- "CGIHTTPServer",
- "Carbon",
- "ColorPicker",
- "ConfigParser",
- "Cookie",
- "DEVICE",
- "DocXMLRPCServer",
- "EasyDialogs",
- "FL",
- "FrameWork",
- "GL",
- "HTMLParser",
- "MacOS",
- "MimeWriter",
- "MiniAEFrame",
- "Queue",
- "SUNAUDIODEV",
- "ScrolledText",
- "SimpleHTTPServer",
- "SimpleXMLRPCServer",
- "SocketServer",
- "StringIO",
- "Tix",
- "Tkinter",
- "UserDict",
- "UserList",
- "UserString",
- "__builtin__",
- "__future__",
- "__main__",
- "_winreg",
- "abc",
- "aepack",
- "aetools",
- "aetypes",
- "aifc",
- "al",
- "anydbm",
- "argparse",
- "array",
- "ast",
- "asynchat",
- "asyncore",
- "atexit",
- "audioop",
- "autoGIL",
- "base64",
- "bdb",
- "binascii",
- "binhex",
- "bisect",
- "bsddb",
- "bz2",
- "cPickle",
- "cProfile",
- "cStringIO",
- "calendar",
- "cd",
- "cgi",
- "cgitb",
- "chunk",
- "cmath",
- "cmd",
- "code",
- "codecs",
- "codeop",
- "collections",
- "colorsys",
- "commands",
- "compileall",
- "compiler",
- "contextlib",
- "cookielib",
- "copy",
- "copy_reg",
- "crypt",
- "csv",
- "ctypes",
- "curses",
- "datetime",
- "dbhash",
- "dbm",
- "decimal",
- "difflib",
- "dircache",
- "dis",
- "distutils",
- "dl",
- "doctest",
- "dumbdbm",
- "dummy_thread",
- "dummy_threading",
- "email",
- "ensurepip",
- "errno",
- "fcntl",
- "filecmp",
- "fileinput",
- "findertools",
- "fl",
- "flp",
- "fm",
- "fnmatch",
- "formatter",
- "fpectl",
- "fpformat",
- "fractions",
- "ftplib",
- "functools",
- "future_builtins",
- "gc",
- "gdbm",
- "gensuitemodule",
- "getopt",
- "getpass",
- "gettext",
- "gl",
- "glob",
- "grp",
- "gzip",
- "hashlib",
- "heapq",
- "hmac",
- "hotshot",
- "htmlentitydefs",
- "htmllib",
- "httplib",
- "ic",
- "imageop",
- "imaplib",
- "imgfile",
- "imghdr",
- "imp",
- "importlib",
- "imputil",
- "inspect",
- "io",
- "itertools",
- "jpeg",
- "json",
- "keyword",
- "linecache",
- "locale",
- "logging",
- "macostools",
- "macpath",
- "mailbox",
- "mailcap",
- "marshal",
- "math",
- "md5",
- "mhlib",
- "mimetools",
- "mimetypes",
- "mimify",
- "mmap",
- "modulefinder",
- "msilib",
- "msvcrt",
- "multifile",
- "multiprocessing",
- "mutex",
- "netrc",
- "new",
- "nis",
- "nntplib",
- "ntpath",
- "numbers",
- "operator",
- "optparse",
- "os",
- "os2emxpath",
- "ossaudiodev",
- "parser",
- "pdb",
- "pickle",
- "pickletools",
- "pipes",
- "pkgutil",
- "platform",
- "plistlib",
- "popen2",
- "poplib",
- "posix",
- "posixfile",
- "posixpath",
- "pprint",
- "profile",
- "pstats",
- "pty",
- "pwd",
- "py_compile",
- "pyclbr",
- "pydoc",
- "quopri",
- "random",
- "re",
- "readline",
- "repr",
- "resource",
- "rexec",
- "rfc822",
- "rlcompleter",
- "robotparser",
- "runpy",
- "sched",
- "select",
- "sets",
- "sgmllib",
- "sha",
- "shelve",
- "shlex",
- "shutil",
- "signal",
- "site",
- "smtpd",
- "smtplib",
- "sndhdr",
- "socket",
- "spwd",
- "sqlite3",
- "ssl",
- "stat",
- "statvfs",
- "string",
- "stringprep",
- "struct",
- "subprocess",
- "sunau",
- "sunaudiodev",
- "symbol",
- "symtable",
- "sys",
- "sysconfig",
- "syslog",
- "tabnanny",
- "tarfile",
- "telnetlib",
- "tempfile",
- "termios",
- "test",
- "textwrap",
- "thread",
- "threading",
- "time",
- "timeit",
- "token",
- "tokenize",
- "trace",
- "traceback",
- "ttk",
- "tty",
- "turtle",
- "types",
- "unicodedata",
- "unittest",
- "urllib",
- "urllib2",
- "urlparse",
- "user",
- "uu",
- "uuid",
- "warnings",
- "wave",
- "weakref",
- "webbrowser",
- "whichdb",
- "winsound",
- "wsgiref",
- "xdrlib",
- "xml",
- "xmlrpclib",
- "zipfile",
- "zipimport",
- "zlib",
-)
-
# List of all modules (standard library) available in Python 3.
STDLIB_PY3 = (
"__future__",
@@ -335,6 +53,7 @@
"doctest",
"dummy_threading",
"email",
+ "encodings",
"ensurepip",
"enum",
"errno",
@@ -516,6 +235,8 @@
"allure-pytest": ["allure"],
"ansicolors": ["colors"],
"apache-airflow": ["airflow"],
+ "appengine-python-standard": ["google.appengine"],
+ "atlassian-python-api": ["atlassian"],
"attrs": ["attr", "attrs"],
"awesome-slugify": ["slugify"],
"azure-common": ["azure.common"],
@@ -574,7 +295,9 @@
"ffmpeg-python": ["ffmpeg"],
"fluent-logger": ["fluent"],
"gitpython": ["git"],
+ "google-api-core": ["google.api_core"],
"google-api-python-client": ["apiclient", "googleapiclient"],
+ "google-auth": ["google.auth", "google.oauth2"],
"google-cloud-aiplatform": ["google.cloud.aiplatform"],
"google-cloud-bigquery": ["google.cloud.bigquery"],
"google-cloud-bigtable": ["google.cloud.bigtable"],
@@ -590,7 +313,12 @@
"google.cloud.logging_v2",
"google.cloud.logging",
],
- "google-cloud-pubsub": ["google.cloud.pubsub_v1", "google.cloud.pubsub"],
+ "google-cloud-pubsub": [
+ "google.cloud.pubsub_v1",
+ "google.cloud.pubsub",
+ "google.pubsub_v1",
+ "google.pubsub",
+ ],
"google-cloud-secret-manager": ["google.cloud.secretmanager"],
"google-cloud-storage": ["google.cloud.storage"],
"grpcio": ["grpc"],
@@ -654,14 +382,22 @@
"opentelemetry-sdk": ["opentelemetry.sdk"],
"opentelemetry-test-utils": ["opentelemetry.test"],
"paho-mqtt": ["paho"],
+ "phonenumberslite": ["phonenumbers"],
"pillow": ["PIL"],
"pillow-simd": ["PIL"],
"pip-tools": ["piptools"],
+ "plotly": [
+ "jupyterlab_plotly",
+ "plotly",
+ "_plotly_utils",
+ "_plotly_future_",
+ ],
"progressbar2": ["progressbar"],
"protobuf": ["google.protobuf"],
"psycopg2-binary": ["psycopg2"],
"py-lru-cache": ["lru"],
"pycrypto": ["Crypto"],
+ "pycryptodome": ["Crypto"],
"pygithub": ["github"],
"pygobject": ["gi", "pygtkcompat"],
"pyhamcrest": ["hamcrest"],
@@ -677,16 +413,8 @@
"pyside6": ["PySide6"],
"pytest": ["pytest", "_pytest"],
"pytest-runner": ["ptr"],
- "python-dateutil": ["dateutil"],
- "python-docx": ["docx"],
- "python-dotenv": ["dotenv"],
- "python-hcl2": ["hcl2"],
- "python-jose": ["jose"],
"python-levenshtein": ["Levenshtein"],
"python-lsp-jsonrpc": ["pylsp_jsonrpc"],
- "python-magic": ["magic"],
- "python-pptx": ["pptx"],
- "python-socketio": ["socketio"],
"pyturbojpeg": ["turbojpeg"],
"pyyaml": ["yaml"],
"scikit-fda": ["skfda"],
diff --git a/test/test_checker.py b/test/test_checker.py
index b0483c0..cf83324 100644
--- a/test/test_checker.py
+++ b/test/test_checker.py
@@ -15,6 +15,7 @@ def __init__(self):
"bar",
"hyp-hen",
"python-boom",
+ "python-snake",
"pillow",
"space.module",
],
@@ -27,9 +28,9 @@ class Flake8Checker(checker.Flake8Checker):
def get_setup_py(cls):
return SetupVisitorMock()
- @property
- def processing_setup_py(self):
- return self.filename == "setup.py"
+ @staticmethod
+ def is_project_setup_py(project_root_dir, filename):
+ return filename == "setup.py"
class Flake8OptionManagerMock(dict):
@@ -90,6 +91,10 @@ def test_3rd_party_python_prefix(self):
errors = check("from boom import blast")
self.assertEqual(len(errors), 0)
+ def test_3rd_party_python_prefix_no_strip(self):
+ errors = check("import python_snake as snake")
+ self.assertEqual(len(errors), 0)
+
def test_3rd_party_missing(self):
errors = check("import os\nfrom cat import Cat")
self.assertEqual(len(errors), 1)
diff --git a/test/test_pep621.py b/test/test_pep621.py
index cf0a52a..22e9bc3 100644
--- a/test/test_pep621.py
+++ b/test/test_pep621.py
@@ -1,16 +1,14 @@
import unittest
+from unittest import mock
+from unittest.mock import mock_open
+from unittest.mock import patch
+
+from pkg_resources import parse_requirements
from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import ModuleSet
from flake8_requirements.checker import memoize
-try:
- from unittest import mock
- builtins_open = 'builtins.open'
-except ImportError:
- import mock
- builtins_open = '__builtin__.open'
-
class Flake8Options:
known_modules = ""
@@ -21,7 +19,7 @@ class Flake8Options:
class Pep621TestCase(unittest.TestCase):
- content = """
+ content = b"""
[project]
name="test"
dependencies=["tools==1.0"]
@@ -46,7 +44,7 @@ class Options(Flake8Options):
)
def test_get_pyproject_toml_pep621(self):
- with mock.patch(builtins_open, mock.mock_open(read_data=self.content)):
+ with mock.patch('builtins.open', mock_open(read_data=self.content)):
pep621 = Flake8Checker.get_pyproject_toml_pep621()
expected = {
"name": "test",
@@ -58,16 +56,16 @@ def test_get_pyproject_toml_pep621(self):
self.assertDictEqual(pep621, expected)
def test_get_pyproject_toml_invalid(self):
- content = self.content + "invalid"
- with mock.patch(builtins_open, mock.mock_open(read_data=content)):
+ content = self.content + b"invalid"
+ with mock.patch('builtins.open', mock_open(read_data=content)):
self.assertDictEqual(Flake8Checker.get_pyproject_toml_pep621(), {})
def test_1st_party(self):
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = (
IOError("No such file or directory: 'setup.py'"),
IOError("No such file or directory: 'setup.cfg'"),
- mock.mock_open(read_data=self.content).return_value,
+ mock_open(read_data=self.content).return_value,
)
checker = Flake8Checker(None, None)
@@ -75,13 +73,97 @@ def test_1st_party(self):
self.assertEqual(mods, ModuleSet({"test": {}}))
def test_3rd_party(self):
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = (
IOError("No such file or directory: 'setup.py'"),
IOError("No such file or directory: 'setup.cfg'"),
- mock.mock_open(read_data=self.content).return_value,
+ mock_open(read_data=self.content).return_value,
)
checker = Flake8Checker(None, None)
mods = checker.get_mods_3rd_party(False)
self.assertEqual(mods, ModuleSet({"tools": {}, "dev_tools": {}}))
+
+ def test_dynamic_requirements(self):
+ requirements_content = "package1\npackage2>=2.0"
+ data = {
+ "project": {"dynamic": ["dependencies"]},
+ "tool": {
+ "setuptools": {
+ "dynamic": {"dependencies": {"file": ["requirements.txt"]}}
+ }
+ },
+ }
+ with patch(
+ 'flake8_requirements.checker.Flake8Checker.get_pyproject_toml',
+ return_value=data,
+ ):
+ with patch(
+ 'builtins.open', mock_open(read_data=requirements_content)
+ ):
+ result = Flake8Checker.get_setuptools_dynamic_requirements()
+ expected_results = ['package1', 'package2>=2.0']
+ parsed_results = [str(req) for req in result]
+ self.assertEqual(parsed_results, expected_results)
+
+ def test_dynamic_optional_dependencies(self):
+ data = {
+ "project": {"dynamic": ["dependencies", "optional-dependencies"]},
+ "tool": {
+ "setuptools": {
+ "dynamic": {
+ "dependencies": {"file": ["requirements.txt"]},
+ "optional-dependencies": {
+ "test": {"file": ["optional-requirements.txt"]}
+ },
+ }
+ }
+ },
+ }
+ requirements_content = """
+ package1
+ package2>=2.0
+ """
+ optional_requirements_content = "package3[extra] >= 3.0"
+ with mock.patch(
+ 'flake8_requirements.checker.Flake8Checker.get_pyproject_toml',
+ return_value=data,
+ ):
+ with mock.patch('builtins.open', mock.mock_open()) as mocked_file:
+ mocked_file.side_effect = [
+ mock.mock_open(
+ read_data=requirements_content
+ ).return_value,
+ mock.mock_open(
+ read_data=optional_requirements_content
+ ).return_value,
+ ]
+ result = Flake8Checker.get_setuptools_dynamic_requirements()
+ expected = list(parse_requirements(requirements_content))
+ expected += list(
+ parse_requirements(optional_requirements_content)
+ )
+
+ self.assertEqual(len(result), len(expected))
+ for i in range(len(result)):
+ self.assertEqual(result[i], expected[i])
+
+ def test_missing_requirements_file(self):
+ data = {
+ "project": {"dynamic": ["dependencies"]},
+ "tool": {
+ "setuptools": {
+ "dynamic": {
+ "dependencies": {
+ "file": ["nonexistent-requirements.txt"]
+ }
+ }
+ }
+ },
+ }
+ with mock.patch(
+ 'flake8_requirements.checker.Flake8Checker.get_pyproject_toml',
+ return_value=data,
+ ):
+ result = Flake8Checker.get_setuptools_dynamic_requirements()
+ self.assertEqual(result, [])
diff --git a/test/test_poetry.py b/test/test_poetry.py
index a387b7d..59b513b 100644
--- a/test/test_poetry.py
+++ b/test/test_poetry.py
@@ -1,16 +1,11 @@
import unittest
+from unittest import mock
+from unittest.mock import mock_open
from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import ModuleSet
from flake8_requirements.checker import memoize
-try:
- from unittest import mock
- builtins_open = 'builtins.open'
-except ImportError:
- import mock
- builtins_open = '__builtin__.open'
-
class PoetryTestCase(unittest.TestCase):
@@ -18,19 +13,19 @@ def setUp(self):
memoize.mem = {}
def test_get_pyproject_toml_poetry(self):
- content = "[tool.poetry]\nname='x'\n[tool.poetry.tag]\nx=0\n"
- with mock.patch(builtins_open, mock.mock_open(read_data=content)):
+ content = b"[tool.poetry]\nname='x'\n[tool.poetry.tag]\nx=0\n"
+ with mock.patch('builtins.open', mock_open(read_data=content)):
poetry = Flake8Checker.get_pyproject_toml_poetry()
self.assertDictEqual(poetry, {'name': "x", 'tag': {'x': 0}})
def test_1st_party(self):
- content = "[tool.poetry]\nname='book'\n"
+ content = b"[tool.poetry]\nname='book'\n"
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = (
IOError("No such file or directory: 'setup.py'"),
IOError("No such file or directory: 'setup.cfg'"),
- mock.mock_open(read_data=content).return_value,
+ mock_open(read_data=content).return_value,
)
checker = Flake8Checker(None, None)
@@ -38,14 +33,14 @@ def test_1st_party(self):
self.assertEqual(mods, ModuleSet({"book": {}}))
def test_3rd_party(self):
- content = "[tool.poetry.dependencies]\ntools='1.0'\n"
- content += "[tool.poetry.dev-dependencies]\ndev-tools='1.0'\n"
+ content = b"[tool.poetry.dependencies]\ntools='1.0'\n"
+ content += b"[tool.poetry.dev-dependencies]\ndev-tools='1.0'\n"
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = (
IOError("No such file or directory: 'setup.py'"),
IOError("No such file or directory: 'setup.cfg'"),
- mock.mock_open(read_data=content).return_value,
+ mock_open(read_data=content).return_value,
)
checker = Flake8Checker(None, None)
@@ -53,14 +48,14 @@ def test_3rd_party(self):
self.assertEqual(mods, ModuleSet({"tools": {}, "dev_tools": {}}))
def test_3rd_party_groups(self):
- content = "[tool.poetry.dependencies]\ntools='1.0'\n"
- content += "[tool.poetry.group.dev.dependencies]\ndev-tools='1.0'\n"
+ content = b"[tool.poetry.dependencies]\ntools='1.0'\n"
+ content += b"[tool.poetry.group.dev.dependencies]\ndev-tools='1.0'\n"
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = (
IOError("No such file or directory: 'setup.py'"),
IOError("No such file or directory: 'setup.cfg'"),
- mock.mock_open(read_data=content).return_value,
+ mock_open(read_data=content).return_value,
)
checker = Flake8Checker(None, None)
diff --git a/test/test_requirements.py b/test/test_requirements.py
index 99a3094..b31801d 100644
--- a/test/test_requirements.py
+++ b/test/test_requirements.py
@@ -1,30 +1,25 @@
import os
import unittest
from collections import OrderedDict
+from unittest import mock
+from unittest.mock import mock_open
from pkg_resources import parse_requirements
from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import memoize
-try:
- from unittest import mock
- builtins_open = 'builtins.open'
-except ImportError:
- import mock
- builtins_open = '__builtin__.open'
-
def mock_open_with_name(read_data="", name="file.name"):
"""Mock open call with a specified `name` attribute."""
- m = mock.mock_open(read_data=read_data)
+ m = mock_open(read_data=read_data)
m.return_value.name = name
return m
def mock_open_multiple(files=OrderedDict()):
"""Create a mock open object for multiple files."""
- m = mock.mock_open()
+ m = mock_open()
m.side_effect = [
mock_open_with_name(read_data=content, name=name).return_value
for name, content in files.items()
@@ -60,7 +55,7 @@ def test_resolve_requirement_with_file_beyond_max_depth(self):
Flake8Checker.resolve_requirement("-r requirements.txt")
def test_resolve_requirement_with_file_empty(self):
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
self.assertEqual(
Flake8Checker.resolve_requirement("-r requirements.txt", 1),
[],
@@ -68,7 +63,7 @@ def test_resolve_requirement_with_file_empty(self):
m.assert_called_once_with("requirements.txt")
def test_resolve_requirement_with_file_content(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo >= 1.0.0\nbar <= 1.0.0\n"),
)))):
self.assertEqual(
@@ -77,7 +72,7 @@ def test_resolve_requirement_with_file_content(self):
)
def test_resolve_requirement_with_file_content_line_continuation(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo[bar] \\\n>= 1.0.0\n"),
)))):
self.assertEqual(
@@ -86,7 +81,7 @@ def test_resolve_requirement_with_file_content_line_continuation(self):
)
def test_resolve_requirement_with_file_content_line_continuation_2(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo \\\n>= 1.0.0 \\\n# comment \\\nbar \\"),
)))):
self.assertEqual(
@@ -95,14 +90,14 @@ def test_resolve_requirement_with_file_content_line_continuation_2(self):
)
def test_resolve_requirement_with_file_recursion_beyond_max_depth(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "-r requirements.txt\n"),
)))):
with self.assertRaises(RuntimeError):
Flake8Checker.resolve_requirement("-r requirements.txt", 1),
def test_resolve_requirement_with_file_recursion(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "--requirement inner.txt\nbar <= 1.0.0\n"),
("inner.txt", "# inner\nbaz\n\nqux\n"),
)))):
@@ -112,7 +107,7 @@ def test_resolve_requirement_with_file_recursion(self):
)
def test_resolve_requirement_with_relative_include(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "-r requirements/production.txt"),
("requirements/production.txt", "-r node/one.txt\nfoo"),
("requirements/node/one.txt", "-r common.txt\n-r /abs/path.txt"),
@@ -132,13 +127,13 @@ def test_resolve_requirement_with_relative_include(self):
])
def test_init_with_no_requirements(self):
- with mock.patch(builtins_open, mock.mock_open()) as m:
+ with mock.patch('builtins.open', mock_open()) as m:
m.side_effect = IOError("No such file or directory"),
checker = Flake8Checker(None, None)
self.assertEqual(checker.get_requirements_txt(), ())
def test_init_with_user_requirements(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements/base.txt", "foo >= 1.0.0\n-r inner.txt\n"),
("requirements/inner.txt", "bar\n"),
)))) as m:
@@ -160,7 +155,7 @@ def test_init_with_user_requirements(self):
Flake8Checker.requirements_file = None
def test_init_with_simple_requirements(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo >= 1.0.0\nbar <= 1.0.0\n"),
)))):
checker = Flake8Checker(None, None)
@@ -173,7 +168,7 @@ def test_init_with_simple_requirements(self):
)
def test_init_with_recursive_requirements_beyond_max_depth(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo >= 1.0.0\n-r inner.txt\nbar <= 1.0.0\n"),
("inner.txt", "# inner\nbaz\n\nqux\n"),
)))):
@@ -186,7 +181,7 @@ def test_init_with_recursive_requirements_beyond_max_depth(self):
Flake8Checker.requirements_max_depth = 1
def test_init_with_recursive_requirements(self):
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo >= 1.0.0\n-r inner.txt\nbar <= 1.0.0\n"),
("inner.txt", "# inner\nbaz\n\nqux\n"),
)))):
@@ -205,7 +200,7 @@ def test_init_misc(self):
curdir = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(curdir, "test_requirements.txt")) as f:
requirements_content = f.read()
- with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
+ with mock.patch('builtins.open', mock_open_multiple(files=OrderedDict((
("requirements.txt", requirements_content),
)))):
checker = Flake8Checker(None, None)
diff --git a/test/test_setup.py b/test/test_setup.py
index 83dc89f..3a23e2d 100644
--- a/test/test_setup.py
+++ b/test/test_setup.py
@@ -1,19 +1,14 @@
import ast
import os
import unittest
+from unittest import mock
+from unittest.mock import mock_open
from pkg_resources import parse_requirements
from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import SetupVisitor
-try:
- from unittest import mock
- builtins_open = 'builtins.open'
-except ImportError:
- import mock
- builtins_open = '__builtin__.open'
-
class SetupTestCase(unittest.TestCase):
@@ -100,7 +95,7 @@ def test_get_setup_cfg_requirements(self):
curdir = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(curdir, "test_setup.cfg")) as f:
content = f.read()
- with mock.patch(builtins_open, mock.mock_open(read_data=content)):
+ with mock.patch('builtins.open', mock_open(read_data=content)):
checker = Flake8Checker(None, None)
self.assertEqual(
checker.get_setup_cfg_requirements(False),
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..00304f2
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,35 @@
+[tox]
+envlist =
+ coverage
+ py3
+isolated_build = true
+
+[testenv]
+description = Run the tests with pytest under {basepython}.
+setenv =
+ COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
+commands =
+ pytest \
+ --cov="{envsitepackagesdir}/flake8_requirements" \
+ --cov-config="{toxinidir}/tox.ini" \
+ test
+deps =
+ pytest
+ pytest-cov
+
+[testenv:coverage]
+description = Combine coverage data and create final XML report.
+setenv =
+ COVERAGE_FILE = {toxworkdir}/.coverage
+commands =
+ coverage combine
+ coverage report
+ coverage xml -o "{toxworkdir}/coverage.xml"
+skip_install = true
+deps = coverage
+depends = py3
+
+[coverage:paths]
+source = src/flake8_requirements
+ */.tox/*/lib/python*/site-packages/flake8_requirements
+ */src/flake8_requirements