Skip to content
This repository has been archived by the owner on Nov 3, 2023. It is now read-only.

Correctly detect publicity of modules inside directories #494

Merged
merged 25 commits into from Aug 22, 2020

Conversation

gbroques
Copy link
Contributor

Continuation of PR #470 as @tibdex was taking a little to respond, and their PR relates to issue #493 that I opened.

I branched off @tibdex's work so they get contribution credits for the work they did. Thank you @tibdex!!

@samj1912 @Nurdok Please review. This uses pathlib in the parser_test.py now.

src/tests/parser_test.py Outdated Show resolved Hide resolved
samj1912
samj1912 previously approved these changes Jul 20, 2020
docs/release_notes.rst Outdated Show resolved Hide resolved
@gbroques
Copy link
Contributor Author

gbroques commented Jul 20, 2020

@samj1912 What's your expectation of these tests?

    module = parser.parse(code, str(parent_path / "_private_pkg" / "filepath"))
    assert not module.is_public

    module = parser.parse(code, str(parent_path / "_private_pkg" / "some_pkg" / "filepath"))
    assert not module.is_public

My expectation is that these should pass ✔️? (the above tests currently fail ❌)

Thus, a module containing no leading underscore inside a private package should be considered private.


EDIT: Nevermind, according to @Nurdok in this comment and other comments in PR #326, these should be private.

This is addressed in commit 123025c.

@gbroques
Copy link
Contributor Author

gbroques commented Jul 21, 2020

Publicity is inherently affected by where you run pydocstyle.

Consider the following example:

publicpackage/
├── __init__.py
└── _privatepackage/
    └── some_private_module.py  # no docstring here
$ pydocstyle publicpackage/ ↵
  # no errors
$ cd publicpackage/_privatepackage ↵
$ pydocstyle some_private_module.py ↵
some_private_module.py:1 at module level:
        D100: Missing docstring in public module

Maybe we can simply document that publicity is affected by where pydocstyle is executed?

This issue was also brought up in #326 by this comment.

@Nurdok @samj1912 thoughts?

@tibdex
Copy link
Contributor

tibdex commented Jul 27, 2020

I was on vacation, thanks for taking over @gbroques 👍. I'll close my PR.

@gbroques
Copy link
Contributor Author

I was on vacation, thanks for taking over @gbroques 👍. I'll close my PR.

Not a problem! Thank you for your initial work on this.

Hopefully we can get it merged :)

src/pydocstyle/parser.py Outdated Show resolved Hide resolved
@gbroques gbroques requested review from samj1912 and Nurdok July 27, 2020 23:52
@gbroques gbroques requested a review from samj1912 August 6, 2020 00:13
path = Path(self.name).parent # Ignore the actual module's name
syspath = [Path(p) for p in sys.path] # Convert to pathlib.Path.

while path != path.parent and path not in syspath: # Bail if we are at the root directory or in `PYTHONPATH`.
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for the error in my own code, but I guess this exceeds 80 chars? Just move the comment one line up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries, this should be resolved in f807365.

Path("__private_package") / "package" / "module.py",
Path("") / "__private_package" / "package" / "module.py"
))
def test_module_publicity_with_private_paths(private_path):
Copy link
Member

Choose a reason for hiding this comment

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

Please also add some tests with the same path over different sys.path cases.

Copy link
Contributor Author

@gbroques gbroques Aug 6, 2020

Choose a reason for hiding this comment

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

Can you elaborate on how to do this please?


EDIT: It looks like pytest includes a monkeypatch fixture with syspath_prepend() method to modify sys.path.

Is something like this desirable (updated in dde693f)?

@pytest.mark.parametrize("syspath,is_public", (
    ("/", False),
    ("_foo/", True),
))
def test_module_publicity_with_different_sys_path(syspath, is_public, monkeypatch):
    """Test module publicity for same path and different sys.path."""
    parser = Parser()
    code = CodeSnippet("")

    monkeypatch.syspath_prepend(syspath)
    
    path = Path("_foo") / "bar" / "baz.py"
    module = parser.parse(code, str(path))
    assert module.is_public == is_public

@gbroques gbroques force-pushed the fix-module_is_public branch 3 times, most recently from 211088c to 1bda783 Compare August 7, 2020 00:28
@@ -10,7 +10,9 @@ Publicity for all constructs is determined as follows: a construct is
considered *public* if -

1. Its immediate parent is public *and*
Copy link
Member

Choose a reason for hiding this comment

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

Should we also update the docs to include our new behavior?

Copy link
Contributor Author

@gbroques gbroques Aug 8, 2020

Choose a reason for hiding this comment

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

Which new behavior are you referring to?

Do you have suggestions for wording it?

Copy link
Member

Choose a reason for hiding this comment

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

Ah - I meant the behavior around syspath. That may catch some people off guard.

Copy link
Contributor Author

@gbroques gbroques Aug 8, 2020

Choose a reason for hiding this comment

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

I can mention it as a "Note" somewhere in the "How publicity is determined" section.

Note, a construct's parent is recursively checked upward until we reach a directory in sys.path to avoid considering the complete filepath.

Thoughts?

EDIT: I suppose this is dependent upon your other comment.

Copy link
Member

Choose a reason for hiding this comment

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

Let's keep this comment to what you suggested for now.

Copy link
Contributor Author

@gbroques gbroques Aug 10, 2020

Choose a reason for hiding this comment

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

Updated in commit 2f6ab9e.

syspath = [Path(p) for p in sys.path] # Convert to pathlib.Path.

# Bail if we are at the root directory or in `PYTHONPATH`.
while path != path.parent and path not in syspath:
Copy link
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 iterate all the way to the root. But only till the current directory? Thoughts?

Copy link
Member

@samj1912 samj1912 Aug 8, 2020

Choose a reason for hiding this comment

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

edit - The current working directory will be in the sys path in certain conditions https://docs.python.org/3.8/library/sys.html#sys.path

Copy link
Contributor Author

@gbroques gbroques Aug 8, 2020

Choose a reason for hiding this comment

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

I did some tests with the pydocstyle CLI locally, and the directory that you execute pydocstyle from is not included in sys.path.

I don't think we should iterate all the way to the root. But only till the current directory?

Maybe your right? Please advise.

Copy link
Member

Choose a reason for hiding this comment

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

I am just wondering about this use case because if we iterate all they way till root, there might be folders with an underscore in their name. On the other hand this means that pydocstyle will have different outputs on the same input based on the CWD which I am not sure about. So - let's keep it to sys.path for now simply because it makes the checker consistent.

In the future if we decide to change this behavior we can always put in a PR - @Nurdok what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

+1 for keeping as is and documenting it. A user can always get the current-directory-behavior by calling PYTHONPATH=$PYTHONPATH:$CWD pydocstyle ....

samj1912
samj1912 previously approved these changes Aug 10, 2020
Copy link
Member

@samj1912 samj1912 left a comment

Choose a reason for hiding this comment

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

@gbroques Thank you for being so persistent and patient about this PR. I think this is good to go but I would like @Nurdok to take a look before merging since he had the last significant review on this PR.

@gbroques
Copy link
Contributor Author

@gbroques Thank you for being so persistent and patient about this PR. I think this is good to go but I would like @Nurdok to take a look before merging since he had the last significant review on this PR.

No worries! I really appreciate all the excellent feedback you and @Nurdok have provided, and this is just a small way I can say thank you for the work you two do. :)

samj1912
samj1912 previously approved these changes Aug 16, 2020
Nurdok
Nurdok previously approved these changes Aug 22, 2020
@@ -25,6 +27,12 @@ a class called ``_Foo`` is considered private. A method ``bar`` in ``_Foo`` is
also considered private since its parent is a private class, even though its
name does not begin with a single underscore.

Note, a construct's parent is recursively checked upward until we reach a directory
Copy link
Member

Choose a reason for hiding this comment

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

construct -> module?


def _is_inside_private_package(self):
"""Return True if the module is inside a private package."""
path = Path(self.name).parent # Ignore the actual module's name
Copy link
Member

Choose a reason for hiding this comment

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

Add a dot at the end of the comment please (my bad I guess).

docs/snippets/publicity.rst Outdated Show resolved Hide resolved
src/pydocstyle/parser.py Outdated Show resolved Hide resolved
@samj1912 samj1912 dismissed stale reviews from Nurdok and themself via 0e5648c August 22, 2020 17:36
@samj1912 samj1912 merged commit 2aa3aa7 into PyCQA:master Aug 22, 2020
@samj1912
Copy link
Member

@gbroques sorry for the long wait. Thanks for this PR!

@gbroques
Copy link
Contributor Author

@samj1912 thank you!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants