Skip to content

Commit

Permalink
Allow user to set the path to requirements file
Browse files Browse the repository at this point in the history
There is no standard name for the so called requirements.txt file.
Also, there is no standard location for such a file. From now, it
will be possible to specify file name/location from command line.

When a command line option with requirements text file is given, it
will disable requirements search in the setup.py and pyproject.toml
files.

Fixes arkq#26 and resolves arkq#33
  • Loading branch information
arkq committed Mar 27, 2021
1 parent 6f3b801 commit 569de73
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 6 deletions.
8 changes: 6 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ In order to collect project's dependencies, this checker evaluates Python code f
`eval() <https://docs.python.org/3/library/functions.html#eval>`_ function. As a fall-back
method, this checker also tries to load dependencies from the ``pyproject.toml`` file from
the `poetry <https://python-poetry.org/>`_ tool section, or from the ``requirements.txt``
text file.
text file in the project's root directory.

At this point it is very important to be aware of the consequences of the above approach. One
might inject malicious code into the ``setup.py`` file, which will be executed by this checker.
Expand Down Expand Up @@ -68,7 +68,11 @@ disabled by default, but user can enable it with the ``--scan-host-site-packages
option. Please note, however, that the location of the site-packages directory will be determined
by the Python version used for flake8 execution.

If you use the ``-r`` flag in your ``requirements.txt`` file with more than one level of recursion
In order to read requirements from the text file, user shall provide the location of such a file
with the ``--requirements-file`` option. If the given location is not an absolute path, then it
has to be specified as a path relative to the project's root directory.

If you use the ``-r`` flag in your requirements text file with more than one level of recursion
(in other words, one file includes another, the included file includes yet another, and so on),
add the ``--requirements-max-depth`` option to flake8 (for example, ``--requirements-max-depth=3``
to allow three levels of recursion).
30 changes: 26 additions & 4 deletions src/flake8_requirements/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .modules import STDLIB_PY3

# NOTE: Changing this number will alter package version as well.
__version__ = "1.3.3"
__version__ = "1.4.0"
__license__ = "MIT"

LOG = getLogger('flake8.plugin.requirements')
Expand Down Expand Up @@ -295,6 +295,9 @@ class Flake8Checker(object):
# User defined project->modules mapping.
known_modules = {}

# User provided requirements file.
requirements_file = None

# Max depth to resolve recursive requirements.
requirements_max_depth = 1

Expand Down Expand Up @@ -324,6 +327,18 @@ def add_options(cls, manager):
),
**kw
)
manager.add_option(
"--requirements-file",
action='store',
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 or pyproject.toml will not"
" be taken into account."
),
**kw
)
manager.add_option(
"--requirements-max-depth",
type="int",
Expand Down Expand Up @@ -355,6 +370,7 @@ def parse_options(cls, options):
for x in re.split(r"],?", options.known_modules)[:-1]
]
}
cls.requirements_file = options.requirements_file
cls.requirements_max_depth = options.requirements_max_depth
if options.scan_host_site_packages:
cls.discover_host_3rd_party_modules()
Expand Down Expand Up @@ -457,12 +473,14 @@ def get_pyproject_toml_poetry(cls):
@memoize
def get_requirements_txt(cls):
"""Try to load requirements from text file."""
path = cls.requirements_file or "requirements.txt"
if not os.path.isabs(path):
path = os.path.join(cls.root_dir, path)
try:
path = os.path.join(cls.root_dir, "requirements.txt")
return tuple(parse_requirements(cls.resolve_requirement(
"-r {}".format(path), cls.requirements_max_depth + 1)))
except IOError as e:
LOG.debug("Couldn't load requirements: %s", e)
LOG.error("Couldn't load requirements: %s", e)
return ()

@classmethod
Expand Down Expand Up @@ -493,6 +511,10 @@ def get_mods_1st_party(cls):
def get_mods_3rd_party_requirements(self):
"""Get list of 3rd party requirements."""

# Use user provided requirements text file.
if self.requirements_file:
return self.get_requirements_txt()

# Use requirements from setup if available.
cfg_setup = self.get_setup_py()
if cfg_setup.redirected:
Expand All @@ -513,7 +535,7 @@ def get_mods_3rd_party_requirements(self):
))
return requirements

# Get requirements from text file.
# Fall-back to requirements.txt in the root directory.
return self.get_requirements_txt()

@memoize
Expand Down
1 change: 1 addition & 0 deletions test/test_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def processing_setup_py(self):

class Flake8Options:
known_modules = ""
requirements_file = None
requirements_max_depth = 0
scan_host_site_packages = False

Expand Down
22 changes: 22 additions & 0 deletions test/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,28 @@ def test_init_with_no_requirements(self):
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((
("requirements/base.txt", "foo >= 1.0.0\n-r inner.txt\n"),
("requirements/inner.txt", "bar\n"),
)))) as m:
try:
Flake8Checker.requirements_file = "requirements/base.txt"
checker = Flake8Checker(None, None)
self.assertEqual(
checker.get_requirements_txt(),
tuple(parse_requirements([
"foo",
"bar",
])),
)
m.assert_has_calls([
mock.call("requirements/base.txt"),
mock.call("requirements/inner.txt"),
])
finally:
Flake8Checker.requirements_file = None

def test_init_with_simple_requirements(self):
with mock.patch(builtins_open, mock_open_multiple(files=OrderedDict((
("requirements.txt", "foo >= 1.0.0\nbar <= 1.0.0\n"),
Expand Down

0 comments on commit 569de73

Please sign in to comment.