Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict any logics inside __init__.py #36

Closed
sobolevn opened this issue Jul 5, 2018 · 17 comments
Closed

Restrict any logics inside __init__.py #36

sobolevn opened this issue Jul 5, 2018 · 17 comments
Labels
help wanted Extra attention is needed level:starter Good for newcomers rule request Adding a new rule

Comments

@sobolevn
Copy link
Member

sobolevn commented Jul 5, 2018

Side effect inside __init__.py are nightmares.

We need to restrict any kind of python code inside __init__.py except:

  1. comments
  2. docstrings

Imports should be forbidden. Each separate module should have its own public API.

@sobolevn sobolevn added the rule request Adding a new rule label Jul 5, 2018
@mcproger
Copy link
Contributor

@sobolevn I would like to work on this issue.

@sobolevn
Copy link
Member Author

Oh, great!

What I have in mind is:
0. We need to create new error for this types of issues, let it be Z400 (as I reserve Z3xx for classes)

  1. We need to create ModuleFileVisiter to check these kinds of features
  2. Then we need to pass the filename and the ast tree into this visiter
  3. And then check only root-level nodes to be in whitelist (just docs for now)
  4. Raise errors if other things will be there

Other things that will be required:

  1. integration test
  2. noqa example, since sometimes we really need this feature, and we need a way to disable the check for a single file

Thanks for your support!

@sobolevn sobolevn added help wanted Extra attention is needed level:starter Good for newcomers labels Jul 14, 2018
@mcproger
Copy link
Contributor

@sobolevn Sorry, but I have too much other work now. I can still work on this task, but I can't guarantee how soon it will be ready.

@sobolevn
Copy link
Member Author

@mcproger ok

@malinoff
Copy link
Contributor

malinoff commented Aug 1, 2018

What about importing submodules and populating __all__? This is a python code, for sure.

@sobolevn
Copy link
Member Author

sobolevn commented Aug 1, 2018

I prefer not to import any kind of stuff in __init__.py. This way I always use full path to import things.

The only possibility to use this hack is when you have a backwards compatibility with clients you don't have in control. This way you go with # noqa: ... and documentation.

When you have control over your code and moving things around, just refactor all imports and continue.

The same goes for __all__. All regular names are public by default, all underscored names are protected/private by default. So we do not use, and even forbid to use __all__.

@malinoff
Copy link
Contributor

malinoff commented Aug 1, 2018

If you ever want somebody else to use the tool, you have to adopt current practices, one of them is to populate __all__ explicitly.

Maybe we can agree on something in between? For example, to have a check that __init__.py has less than X lines (say, 20 by default).

@sobolevn
Copy link
Member Author

sobolevn commented Aug 1, 2018

@malinoff can you please elaborate on why __all__ is important?

@malinoff
Copy link
Contributor

malinoff commented Aug 1, 2018

I suppose you are looking for a technical argument, not philosophical one (like "explicit is better than implicit").
I have one for you. Consider the following snippet:

import os
import re
import subprocess

def call_some_command(cmd):
  # call the cmd
  # parse output with re
  # do filesystem manipulation

With __all__, only the call_some_command function is exported. Without __all__, as you said, every single name not starting with an underscore is exported which means call_some_command, os, re and subprocess.

I guess it's okay-ish if imported modules are easily recognizable for any python developer. But how are you going to distinguish your public apis from, say, specific 3rd party modules?

@sobolevn
Copy link
Member Author

sobolevn commented Aug 1, 2018

Please, keep in mind that import * is already forbidden.
So, there is no technical need in __all__ property.
Which is only required for these kinds of imports: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package

And indeed, there's no way to distinguish our API from 3rd party modules, imports, etc.
And that's not really a problem for me.

If you really want to make some import protected you can alias it: from os import path as _path.
But, I have not seen any real world usecases for this.

This was referenced Sep 10, 2018
@edmondop
Copy link

Is there a reason why using all is discouraged?

@sobolevn
Copy link
Member Author

@edmondo1984 because it is basically used together with import *, which is not allowed.
We also ask people to use _protected convention over managing public / protected via __all__. So, it is not useful at all in our case.

@sobolevn
Copy link
Member Author

If you find that docs do not explain this, please, feel free to send a PR 🙂

@edmondop
Copy link

edmondop commented Dec 17, 2021

How do you expose the public api of a package? Think about a package parquet with three files inside:

  • init.py
  • reader.py
  • writer.py

You want to expose public api parquet.read_file and parquet.write_file so your __init__.py looks like so

from .reader import read_file
from .writer import write_file

__all__ = ['read_file', 'write_file']

@sobolevn
Copy link
Member Author

from .reader import read_file as read_file
from .writer import write_file as write_file

so, this would be compatible with mypy's implicit_reexport rule.

@edmondop
Copy link

edmondop commented Dec 19, 2021

This fails with complaints :

WPS113 Found same alias import: read_file
F401  '. reader. read_file' imported but unused

Also, if you have a use case like so

from my_package import parquet
from my_package.parquet import reader

this fails,

but moving the second import as a relative one like so:

from my_package import parquet
from parquet import reader
 

would cause mypy to fail

@sobolevn
Copy link
Member Author

Yes, you can disable WPS113 per all __init__.py files or per line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed level:starter Good for newcomers rule request Adding a new rule
Projects
None yet
Development

No branches or pull requests

4 participants