Skip to content

Commit 3342e16

Browse files
itamarofacebook-github-bot
authored andcommitted
Add strict modules docs
Reviewed By: carljm Differential Revision: D35038739 fbshipit-source-id: 27fed27
1 parent d532e95 commit 3342e16

30 files changed

+1398
-0
lines changed

CinderDoc/strict_modules/DOCS

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[strict_modules]
2+
type = WIKI
3+
srcs = [
4+
glob(*.rst)
5+
]
6+
wiki_root_path = Python/Cinder/External_Public/Strict_Modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Class / Instance Conflict
2+
#########################
3+
4+
5+
One of the changes that strict modules introduces is the promotion of instance
6+
members to being class level declarations. For more information on this pattern
7+
see :doc:`../limitations/class_attrs`.
8+
9+
A typical case for this is when you'd like to have a default method implementation
10+
but override it on a per instance basis:
11+
12+
.. code-block:: python
13+
14+
class C:
15+
def f(self):
16+
return 42
17+
18+
a = C()
19+
a.f = lambda: "I'm a special snowflake"
20+
21+
22+
If you attempt this inside of a strict module you'll get an AttributeError that
23+
says "'C' object attribute 'f' is read-only". This is because the instance
24+
doesn't have any place to store the method. You might think that you can declare
25+
the field explicitly as specified in the documentation:
26+
27+
.. code-block:: python
28+
29+
class C:
30+
f: ...
31+
def f(self):
32+
return 42
33+
34+
But instead you'll get an error reported by strict modules stating that there's
35+
a conflict with the variable. To get around this issue you can promote the function
36+
to always be treated as an instance member:
37+
38+
.. code-block:: python
39+
40+
class C:
41+
def __init__(self):
42+
self.f = self.default_f
43+
44+
def default_f(self):
45+
return 42
46+
47+
a = C()
48+
a.f = lambda: "I'm a special snowflake" # Ok, you are a special snowflake
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Modifying External State
2+
########################
3+
4+
Strict modules enforces object :doc:`ownership </guide/limitations/ownership>`,
5+
and will not allow module-level code to modify any object defined
6+
in a different module.
7+
8+
One common example of this is to have a global registry of some sort of
9+
objects:
10+
11+
**methods.py**
12+
13+
.. code-block:: python
14+
15+
import __strict__
16+
17+
ROUTES = list()
18+
19+
def route(f):
20+
ROUTES.append(f)
21+
return f
22+
23+
**routes.py**
24+
25+
.. code-block:: python
26+
27+
import __strict__
28+
29+
from methods import route
30+
31+
@route
32+
def create_user(*args):
33+
...
34+
35+
36+
Here we have one module which is maintaining a global registry, which is
37+
populated as a side effect of importing another module. If for some reason
38+
one module doesn't get imported or if the order of imports changes then the
39+
program's execution can change. When strict modules analyzes this code it will
40+
report a :doc:`/guide/errors/modify_imported_value`.
41+
42+
A better pattern for this is to explicitly register the values in a central
43+
location:
44+
45+
**methods.py**
46+
47+
.. code-block:: python
48+
49+
import __strict__
50+
51+
from routes import create_user
52+
53+
ROUTES = [create_user, ...]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Conversion Tips
2+
###############
3+
4+
This section of the documentation includes common patterns that violate the
5+
limitations of strict modules and solutions you can use to work around them.
6+
7+
.. toctree::
8+
:maxdepth: 1
9+
:titlesonly:
10+
:glob:
11+
12+
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
The @loose_slots decorator
2+
##########################
3+
4+
Instances of strict classes have `__slots__
5+
<https://docs.python.org/3/reference/datamodel.html#slots>`_ automatically
6+
created for them. This means they will raise ``AttributeError`` if you try to
7+
add any attribute to them that isn't declared with a type annotation on the
8+
class itself (e.g. ``attrname: int``) or assigned in the ``__init__`` method.
9+
10+
When initially converting a module to strict, if it is widely-used it can be
11+
hard to verify that there isn't code somewhere tacking extra attributes onto
12+
instances of classes defined in that module. In this case, you can temporarily
13+
place the ``strict_modules.loose_slots`` decorator on the class for a safer
14+
transition. Example:
15+
16+
.. code-block:: python
17+
18+
import __strict__
19+
20+
from compiler.strict.runtime import loose_slots
21+
22+
@loose_slots
23+
class MyClass:
24+
...
25+
26+
This decorator will allow extra attributes to be added to the class, but will
27+
fire a warning when it happens. You can access these warnings by setting a
28+
warnings callback function:
29+
30+
.. code-block:: python
31+
32+
from cinder import cinder_set_warnings_handler
33+
34+
def log_cinder_warning(msg: str, *args: object) -> None:
35+
# ...
36+
37+
cinder_set_warnings_handler(log_cinder_warning)
38+
39+
Typically you'd want to set a warnings handler that logs these warnings somewhere,
40+
then you can deploy some new strict modules using `@loose_slots`,
41+
and once the code has been in production for a bit and you see no warnings
42+
fired, you can safely remove `@loose_slots`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Top-level Module Access
2+
#######################
3+
4+
A common pattern is to import a module and access members from that module:
5+
6+
.. code-block:: python
7+
8+
from useful import submodule
9+
10+
class MyClass(submodule.BaseClass):
11+
pass
12+
13+
If “submodule” is not strict, then we don't know what it is and what side
14+
effects could happen by dotting through it. So this pattern is disallowed
15+
inside of a strict module when importing from a non-strict module. Instead
16+
you can transform the code to:
17+
18+
.. code-block:: python
19+
20+
from useful.submodule import BaseClass
21+
22+
class MyClass(BaseClass):
23+
pass
24+
25+
This will cause any side effects that are possible to occur only when
26+
the non-strict module is imported; the execution of the rest of the
27+
strict module will be known to be side effect free.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
Global Singletons
2+
#################
3+
4+
Sometimes it might be useful to encapsulate a set of functionality into
5+
a class and then have a global singleton of that class. And sometimes
6+
that global singleton might have dependencies on non-strict code which
7+
makes it impossible to construct at the top-level in a strict module.
8+
9+
.. code-block:: python
10+
11+
from non_strict import get_counter_start
12+
13+
class Counter:
14+
def __init__(self) -> None:
15+
self.value: int = get_counter_start()
16+
17+
def next(self) -> int:
18+
res = self.value
19+
self.value += 1
20+
return res
21+
22+
COUNTER = Counter()
23+
24+
One way to address this is to refactor the Counter class so that it
25+
does less when constructed, delaying some work until first use. For
26+
example:
27+
28+
.. code-block:: python
29+
30+
from non_strict import get_counter_start
31+
32+
class Counter:
33+
def __init__(self) -> None:
34+
self.value: int = -1
35+
36+
def next(self) -> int:
37+
if self.value == -1:
38+
self.value = get_counter_start()
39+
res = self.value
40+
self.value += 1
41+
return res
42+
COUNTER = Counter()
43+
44+
Another approach is that instead of constructing the singleton at the
45+
top of the file you can push this into a function so it gets defined
46+
the first time it'll need to be used:
47+
48+
.. code-block:: python
49+
50+
_COUNTER = None
51+
52+
def get_counter() -> Counter:
53+
global _COUNTER
54+
if _COUNTER is None:
55+
_COUNTER = Counter()
56+
return _COUNTER
57+
58+
You can also use an lru_cache instead of a global variable:
59+
60+
.. code-block:: python
61+
62+
from functools import lru_cache
63+
64+
@lru_cache(maxsize=1)
65+
def get_counter() -> Counter:
66+
return Counter()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
Splitting Modules
2+
#################
3+
4+
Sometimes a module might contain functionality which is dependent upon certain
5+
behavior which cannot be analyzed - either it truly has external side effects,
6+
it is dependent upon another module which cannot yet be strictified and needs
7+
to be used at the top-level, or it is dependent upon something which strict
8+
modules have not yet been able to analyze.
9+
10+
In these cases one possible solution, although generally a last resort,
11+
is to break the module into two modules. The first module will only contain
12+
the code which cannot be safely strictified. The second module will contain
13+
all of the code that can be safely treated as strict. A better way to do this
14+
is to not have the unverifable code happen at startup, but if that's not
15+
possible then splitting is an acceptable option.
16+
17+
Because strict modules can still import non-strict modules the strict module
18+
can continue to expose the same interface as it previously did, and no other
19+
code needs to be updated. The only limitation to this is that it requires
20+
that the module being strictified doesn't need to interact with the non-strict
21+
elements at the top level. For example classes could still create instances
22+
of them, but the strict module couldn't call functions in the non-strict
23+
module at the top level.
24+
25+
26+
.. code-block:: python
27+
28+
import csv
29+
from random import choice
30+
31+
FAMOUS_PEOPLE = list(csv.reader(open('famous_people.txt').readlines()))
32+
33+
class FamousPerson:
34+
def __init__(self, name, age, height):
35+
self.name = name
36+
self.age = int(age)
37+
self.height = float(height)
38+
39+
def get_random_person():
40+
return FamousPerson(*choice(FAMOUS_PEOPLE))
41+
42+
43+
We can split this into two modules, one which does the unverifable read of our
44+
sample data from disk and another which returns the random piece of sample data:
45+
46+
47+
.. code-block:: python
48+
49+
import csv
50+
51+
FAMOUS_PEOPLE = list(csv.reader(open('famous_people.txt').readlines()))
52+
53+
54+
And we can have another module which exports our FamousPerson class along with
55+
the API to return a random famous person:
56+
57+
.. code-block:: python
58+
59+
from random import choice
60+
from famous_people_data import FAMOUS_PEOPLE
61+
62+
class FamousPerson:
63+
def __init__(self, name, age, height):
64+
self.name = name
65+
self.age = int(age)
66+
self.height = float(height)
67+
68+
def get_random_person():
69+
return FamousPerson(*choice(FAMOUS_PEOPLE))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
Strict Module Stubs
2+
###################
3+
4+
Sometimes your modules depend on other modules that cannot be directly
5+
strictified - it could depend on a Cython module, or a module from a
6+
third-party library whose source code you can't modify.
7+
8+
In this situation, if you are certain that the dependency is strict, you
9+
can provide a strict module stub file (`.pys`) describing the behavior of
10+
the module. Put the strict module stub file in your strict module stubs directory
11+
(this is configured via `-X strict-module-stubs-path=...=` or
12+
`PYTHONSTRICTMODULESTUBSPATH` env var, or by subclassing `StrictSourceFileLoader`
13+
and passing a `stub_path` argument to `super().__init__(...)`.)
14+
15+
There are two ways to stub a class or function in a strict module stub file.
16+
You can provide a full Python implementation, which is useful in the case
17+
of stubbing a Cython file, or you can just provide a function/class name,
18+
with a `@implicit` decorator. In the latter case, the stub triggers the
19+
strict module analyzer to look for the source code on `sys.path` and analyze
20+
the source code.
21+
22+
If the module you depend on is already actually strict-compliant you can
23+
simplify the stub file down to just contain the single line `__implicit__`,
24+
which just says "go use the real module contents, they're fine".
25+
See `Lib/compiler/strict/stubs/_collections_abc.pys` for an existing example.
26+
Per-class/function stubs are only needed where the stdlib module does
27+
non-strict things at module level, so we need to extract just the bits we
28+
depend on and verify them for strictness.
29+
30+
If both a `.py` file and a `.pys` file exist, the strict module analyzer will
31+
prioritize the `.pys` file. This means adding stubs to existing
32+
modules in your codebase will shadow the actual implementation.
33+
You should probably avoid doing this.
34+
35+
Example of Cython stub:
36+
37+
**myproject/worker.py**
38+
39+
.. code-block:: python
40+
41+
from some_cython_mod import plus1
42+
43+
two = plus1(1)
44+
45+
46+
Here you can provide a stub for the Cython implementation of `plus1`
47+
48+
**strict_modules/stubs/some_cython_mod.pys**
49+
50+
.. code-block:: python
51+
52+
# a full reimplementation of plus1
53+
def plus1(arg):
54+
return arg + 1
55+
56+
Suppose you would like to use the standard library functions `functools.wraps`,
57+
but the strict module analysis does not know of the library. You can add an implicit
58+
stub:
59+
60+
**strict_modules/stubs/functools.pys**
61+
62+
.. code-block:: python
63+
64+
@implicit
65+
def wraps(): ...
66+
67+
You can mix explicit and implicit stubs. See `Lib/compiler/strict/stubs` for some examples.

0 commit comments

Comments
 (0)