Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ contributors:
- Michael Hudson-Doyle <michael.hudson@canonical.com>
- Michael Giuffrida <mgiuffrida@users.noreply.github.com>
- Melvin Hazeleger <31448155+melvio@users.noreply.github.com> (melvio)
- Mehdi Drissi: Added an option to disable sys path patching
- Matěj Grabovský <mgrabovs@redhat.com>
- Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com>
- Matej Marušák <marusak.matej@gmail.com>
Expand Down
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,12 @@ Release date: 2021-11-24
longer relies on counting if statements anymore and uses known if statements locations instead.
It should not crash on badly parsed if statements anymore.

* Add an option ``disable-path-patching`` to better support namespace packages.
This option is disabled by default as it has a trade off with being able
to lint uninstalled packages.

Closes #5266

* Fix ``simplify-boolean-expression`` when condition can be inferred as False.

Closes #5200
Expand Down
16 changes: 14 additions & 2 deletions doc/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,24 @@ behavior. Likewise, since negative values are still technically supported,
``evaluation`` can be set to a version of the above expression that does not
enforce a floor of zero.

6.2 I think I found a bug in Pylint. What should I do?
6.2 I have import errors working with a namespace package. How can I fix it?
------------------------------------------------------------------------------

If you have an implicit namespace package (i.e. a package without ``__init__.py``)
then pylint may not be able to determine base of the package. Normally when you run
pylint it adjusts PYTHONPATH to include the parent directory of the package you are
working in. For namespace packages you can disable path inference with
``--disable-path-patching`` setting. For this to work you must have the namespace package
installed in your environment or run pylint from the directory containing the package.



6.3 I think I found a bug in Pylint. What should I do?
-------------------------------------------------------

Read :ref:`Bug reports, feedback`

6.3 I have a question about Pylint that isn't answered here.
6.4 I have a question about Pylint that isn't answered here.
------------------------------------------------------------

Read :ref:`Mailing lists`
6 changes: 6 additions & 0 deletions doc/whatsnew/2.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ Extensions
Other Changes
=============

* Add an option ``disable-path-patching`` to better support namespace packages.
This option is disabled by default as it has a trade off with being able
to lint uninstalled packages.

Closes #5266

* Only raise ``not-callable`` when all the inferred values of a property are not callable.

Closes #5931
Expand Down
19 changes: 17 additions & 2 deletions pylint/lint/pylinter.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,21 @@ def make_options() -> Tuple[Tuple[str, OptionDict], ...]:
),
},
),
(
"disable-path-patching",
{
"type": "yn",
"metavar": "<y or n>",
"default": False,
"help": (
"Disable the patching of sys.path when searching for "
"modules. This is useful for implicit namespace packages where "
"inferring base path of package is ambiguous. The downside is without "
"patching, the files being linted must be importable from the location "
"the script is run in interpreter."
),
},
),
)

base_option_groups = (
Expand Down Expand Up @@ -1058,13 +1073,13 @@ def check(self, files_or_modules: Union[Sequence[str], str]) -> None:
)

filepath = files_or_modules[0]
with fix_import_path(files_or_modules):
with fix_import_path(files_or_modules, self.config.disable_path_patching):
self._check_files(
functools.partial(self.get_ast, data=_read_stdin()),
[self._get_file_descr_from_stdin(filepath)],
)
elif self.config.jobs == 1:
with fix_import_path(files_or_modules):
with fix_import_path(files_or_modules, self.config.disable_path_patching):
self._check_files(
self.get_ast, self._iterate_file_descrs(files_or_modules)
)
Expand Down
13 changes: 9 additions & 4 deletions pylint/lint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ def preprocess_options(args, search_for):


def _patch_sys_path(args):
original = list(sys.path)
changes = []
seen = set()
for arg in args:
Expand All @@ -118,19 +117,25 @@ def _patch_sys_path(args):
seen.add(path)

sys.path[:] = changes + sys.path
return original


@contextlib.contextmanager
def fix_import_path(args):
def fix_import_path(args, disable_path_patching: bool = False):
"""Prepare 'sys.path' for running the linter checks.

'disable_path_patching' if True will make this context manager a no-op.
The reason for disabling path patching is to allow running the linter
using default path which can be safer then patching the path for implicit
namespace packages.

Within this context, each of the given arguments is importable.
Paths are added to 'sys.path' in corresponding order to the arguments.
We avoid adding duplicate directories to sys.path.
`sys.path` is reset to its original value upon exiting this context.
"""
original = _patch_sys_path(args)
original = list(sys.path)
if not disable_path_patching:
_patch_sys_path(args)
try:
yield
finally:
Expand Down
17 changes: 16 additions & 1 deletion pylint/pyreverse/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,21 @@
help="set the output directory path.",
),
),
(
"disable-path-patching",
dict(
Comment thread
cdce8p marked this conversation as resolved.
type="yn",
metavar="<y or n>",
default=False,
help=(
"Disable the patching of sys.path when searching for "
"modules. This is useful for implicit namespace packages where "
"inferring base path of package is ambiguous. The downside is without "
"patching, the files being linted must be importable from the location "
"the script is run in interpreter."
),
),
),
)


Expand All @@ -217,7 +232,7 @@ def run(self, args):
if not args:
print(self.help())
return 1
with fix_import_path(args):
with fix_import_path(args, self.config.disable_path_patching):
project = project_from_files(
args,
project_name=self.config.project,
Expand Down
12 changes: 12 additions & 0 deletions pylint/testutils/_fake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""This file is intended to only be used for testing namespace package importing.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should add file without purpose in pylint itself. pylint is not a namespace package so maybe we can create a fixture or it's not even required to have one ? Could we use a namespace that does not exists ? Or is this supposed to be in tests/functional/n/namespace_package/pylint/testutils/ ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought testutils was for files used in tests/? Are you fine with test file importing another test file? I don't think that's currently done by any of the tests and a future version of pytest will not allow that by default either.

https://docs.pytest.org/en/6.2.x/pythonpath.html#import-modes
For this reason this doesn’t require test module names to be unique at all, but also makes test modules non-importable by each other

So moving it to tests/functional/n/namespace_package will force you to disable an upcoming pytest change.

There's a second reason why it's better to avoid having the file in tests/. sys path disable mode has a requirement either files you import are installed or you run it from the right directory. tests folder is not included in pytest install. As a side effect if I move the file to tests this new test will only work as expected if run from the repository root directory. If a user tries to run pytest from a different directory for this test it won't work.

Because of those 2 reasons having the _fake file with tests/ will impose restrictions on pytest usage and I'd prefer to avoid. I can move it there, but the tests will become sensitive to where you run them and unsure that's a good trade off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other option is drop this test entirely and make sys path logic is tested in future primer test.

The main issue for making this not directory sensitive is I need module I'm importing to be installed package in repo. The only choice for that is under pylint/ somewhere. I can make a folder _internal/ and actual location doesn't matter as long as it's in pylint.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I forgot to reply. We have some test with import in test_func.py which is the legacy functional test file. Maybe you could take add a test to it exceptionally ? I'm guessing we'll have to think about a system for those kind of tesst soon if pytest is going to make this impossible. But it will always be possible to call pylint on a directory, as an integration tests, right ? Sorry if this is not in this PR scope. I'm hesitant to drop the test as long as #5173 is not merged. Do you have a list of stable project that we could use to test this ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would putting it in tests/regrtest_data work? This is where I have been putting all data for tests that require additional files.
We should probably update the name of that directory though...

Copy link
Copy Markdown
Contributor Author

@hmc-cs-mdrissi hmc-cs-mdrissi Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it would have same issue as anywhere outside pylint. The root issue is this line, https://github.com/PyCQA/pylint/blob/main/setup.cfg#L57

When pylint is installed as a package, it installs pylint directory. It does not install tests directory. Namespace package solution here works under one of 2 conditions, the imported modules are installed or you need to use right directory to run them from. The second condition means that for development tests will break depending on cd and will complicate test scripts. The first condition means I can't test this well if pylint separates test folder from the install.

Some codebases have a test structure of keeping tests in same folder as src and tests are included with a package install. https://docs.pytest.org/en/6.2.x/goodpractices.html#tests-as-part-of-application-code pytest discusses different test folder structures here. The structure of mixing tests with application code would work. It'd need a pr to refactor the test structure. You could in theory mix both test structures but it'd be fairly confusing if some tests lived in pylint/ and other tests lived in tests/. Having every user be forced to install all tests as part of pip install is a downside to this option.

For stable project to test with I'm unsure of repository that uses implicit namespace packages and would fail without this. Most repositories don't use namespace packages. For the ones that do I expect if they use pylint they have workarounds. My internal repo that needs this uses a workaround of never calling pylint on the whole repo and also has multiple setup.py in it which primer would struggle with. The primer I think is designed for repositories with single setup.py/cfg. Monorepo that has multiple separate packages (so separate setup.cfg/py) installed to same namespace I don't think mypy primer supports and that's situation most likely to need this.

I can make a toy repository for this that primer would work with.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Reading the project,

When using multiple namespace packages within the same repository:

Because mynamespace doesn’t contain an init.py, setuptools.find_packages() won’t find the sub-package. You must use setuptools.find_namespace_packages() instead or explicitly list all packages in your setup.py. For example:

is true in readme, but the repository does not follow that itself.

https://github.com/madhavhugar/example-python-namespace-package/blob/master/setup.py#L11

I don't see any namespace module in that repository. Instead it looks like a repository that has 3 packages all in same repository and controlled by same setup.py, but no namespace that shares them.

I can make a simple example for the primer tomorrow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you much appreciated !


Importing a file has a global side effect of adding a module to sys.modules.
As namespace packages are sensitive to sys.path, to test them we check
an intentional import error and a successful import. To avoid the imports
messing up the test environment, we need to make the import error use
a file that is never imported by any other test. This serves as that file.
"""
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# pylint: disable=missing-docstring,unused-import
from pylint.testutils import _fake # [no-name-in-module]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
no-name-in-module:2:0::No name '_fake' in module 'pylint.testutils':HIGH
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# pylint: disable=missing-docstring,unused-import
from pylint import lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[master]
disable-path-patching=yes