Skip to content

Commit 3bb1aaa

Browse files
authored
Merge pull request #88 from dflook/annotations
Make remove annotations transform configurable
2 parents 5398c1a + 2132cd5 commit 3bb1aaa

File tree

14 files changed

+328
-62
lines changed

14 files changed

+328
-62
lines changed

.github/workflows/release_test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ jobs:
112112
- name: Test typing
113113
run: |
114114
pip3.11 install mypy types-setuptools
115-
mypy typing_test/test_typing.py
115+
mypy --strict typing_test/test_typing.py
116116
117-
if mypy typing_test/test_badtyping.py; then
117+
if mypy --strict typing_test/test_badtyping.py; then
118118
echo "Bad types weren't detected"
119119
exit 1
120120
fi

docs/source/api_usage.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ Package Reference
22
=================
33

44
.. automodule:: python_minifier
5-
:members: minify, awslambda, unparse, UnstableMinification
5+
6+
.. autofunction:: minify
7+
.. autoclass:: RemoveAnnotationsOptions
8+
.. autofunction:: awslambda
9+
.. autofunction:: unparse
10+
.. autoclass:: UnstableMinification

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def create_example(option):
8989
#
9090
# This is also used if you do content translation via gettext catalogs.
9191
# Usually you set "language" from the command line for these cases.
92-
language = None
92+
language = 'en'
9393

9494
# List of patterns, relative to source directory, that match files and
9595
# directories to ignore when looking for source files.
@@ -116,7 +116,7 @@ def create_example(option):
116116
# Add any paths that contain custom static files (such as style sheets) here,
117117
# relative to this directory. They are copied after the builtin static files,
118118
# so a file named "default.css" will overwrite the builtin "default.css".
119-
html_static_path = ['_static']
119+
html_static_path = []
120120

121121
# Custom sidebar templates, must be a dictionary that maps document names
122122
# to template names.
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
def compile(source: "something compilable",
2-
filename: "where the compilable thing comes from",
3-
mode: "A string annotation"):
4-
a: int = 1
5-
b: str
6-
7-
def inner():
8-
nonlocal b
9-
b = 'hello'
10-
11-
inner()
12-
print(b)
13-
14-
compile(None, None, None)
1+
class A:
2+
b: int
3+
c: int=2
4+
def a(self, val: str) -> None:
5+
b: int
6+
c: int=2

docs/source/transforms/remove_annotations.rst

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,47 @@
11
Remove Annotations
22
==================
33

4-
This transform removes function annotations and variable annotations.
5-
6-
This transform is generally safe to use. Although the annotations have no meaning to the python language,
4+
This transform removes annotations. Although the annotations have no meaning to the python language,
75
they are made available at runtime. Some python library features require annotations to be kept.
86

9-
If these are detected, annotations are kept for that class:
7+
Annotations can be removed from:
8+
9+
- Function arguments
10+
- Function return
11+
- Variables
12+
- Class attributes
13+
14+
By default annotations are removed from variables, function arguments and function return, but not from class attributes.
15+
16+
This transform is generally safe to use with the default options. If you know the module requires the annotations to be kept, disable this transform.
17+
Class attribute annotations can often be used by other modules, so it is recommended to keep them unless you know they are not used.
18+
19+
When removing class attribute annotations is enabled, annotations are kept for classes that are derived from:
1020

1121
- dataclasses.dataclass
1222
- typing.NamedTuple
1323
- typing.TypedDict
1424

15-
If you know the module requires the annotations to be kept, disable this transform.
16-
1725
If a variable annotation without assignment is used the annotation is changed to a literal zero instead of being removed.
1826

19-
The transform is enabled by default.
20-
Disable this source transformation by passing the ``remove_annotations=False`` argument to the :func:`python_minifier.minify` function,
21-
or passing ``--no-remove-annotations`` to the pyminify command.
27+
Options
28+
-------
29+
30+
These arguments can be used with the pyminify command:
31+
32+
``--no-remove-variable-annotations`` disables removing variable annotations
33+
34+
``--no-remove-return-annotations`` disables removing function return annotations
35+
36+
``--no-remove-argument-annotations`` disables removing function argument annotations
37+
38+
``--remove-class-attribute-annotations`` enables removing class attribute annotations
39+
40+
``--no-remove-annotations`` disables removing all annotations, this transform will not do anything.
41+
42+
When using the :func:`python_minifier.minify` function you can use the ``remove_annotations`` argument to control this transform.
43+
You can pass a boolean ``True`` to remove all annotations or a boolean ``False`` to keep all annotations.
44+
You can also pass a :class:`python_minifier.RemoveAnnotationsOptions` instance to specify which annotations to remove.
2245

2346
Example
2447
-------

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
use_scm_version=True,
2424
package_dir={'': 'src'},
2525
packages=find_packages('src'),
26-
package_data={"python_minifier": ["py.typed", "__init__.pyi"]},
26+
package_data={"python_minifier": ["py.typed", "*.pyi", "rename/*.pyi", "transforms/*.pyi"]},
2727
long_description=long_desc,
2828
long_description_content_type='text/markdown',
2929

src/python_minifier/__init__.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from python_minifier.transforms.combine_imports import CombineImports
2323
from python_minifier.transforms.remove_annotations import RemoveAnnotations
24+
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
2425
from python_minifier.transforms.remove_asserts import RemoveAsserts
2526
from python_minifier.transforms.remove_debug import RemoveDebug
2627
from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
@@ -52,7 +53,7 @@ def __str__(self):
5253
def minify(
5354
source,
5455
filename=None,
55-
remove_annotations=True,
56+
remove_annotations=RemoveAnnotationsOptions(),
5657
remove_pass=True,
5758
remove_literal_statements=False,
5859
combine_imports=True,
@@ -80,7 +81,8 @@ def minify(
8081
:param str source: The python module source code
8182
:param str filename: The original source filename if known
8283
83-
:param bool remove_annotations: If type annotations should be removed where possible
84+
:param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none. RemoveAnnotationsOptions can be used to configure the removal of specific annotations.
85+
:type remove_annotations: bool or RemoveAnnotationsOptions
8486
:param bool remove_pass: If Pass statements should be removed where possible
8587
:param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings
8688
:param bool combine_imports: Combine adjacent import statements where possible
@@ -115,8 +117,20 @@ def minify(
115117
if combine_imports:
116118
module = CombineImports()(module)
117119

118-
if remove_annotations:
119-
module = RemoveAnnotations()(module)
120+
if isinstance(remove_annotations, bool):
121+
remove_annotations_options = RemoveAnnotationsOptions(
122+
remove_variable_annotations=remove_annotations,
123+
remove_return_annotations=remove_annotations,
124+
remove_argument_annotations=remove_annotations,
125+
remove_class_attribute_annotations=remove_annotations,
126+
)
127+
elif isinstance(remove_annotations, RemoveAnnotationsOptions):
128+
remove_annotations_options = remove_annotations
129+
else:
130+
raise TypeError('remove_annotations must be a bool or RemoveAnnotationsOptions')
131+
132+
if remove_annotations_options:
133+
module = RemoveAnnotations(remove_annotations_options)(module)
120134

121135
if remove_pass:
122136
module = RemovePass()(module)

src/python_minifier/__init__.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import ast
2-
from typing import List, Text, AnyStr, Optional, Any
2+
from typing import List, Text, AnyStr, Optional, Any, Union
33

4+
from .transforms.remove_annotations_options import RemoveAnnotationsOptions as RemoveAnnotationsOptions
45

56
class UnstableMinification(RuntimeError):
67
def __init__(self, exception: Any, source: Any, minified: Any): ...
78

89
def minify(
910
source: AnyStr,
1011
filename: Optional[str] = ...,
11-
remove_annotations: bool = ...,
12+
remove_annotations: Union[bool, RemoveAnnotationsOptions] = ...,
1213
remove_pass: bool = ...,
1314
remove_literal_statements: bool = ...,
1415
combine_imports: bool = ...,

src/python_minifier/__main__.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pkg_resources import get_distribution, DistributionNotFound
88

99
from python_minifier import minify
10+
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
1011

1112
try:
1213
version = get_distribution('python_minifier').version
@@ -113,12 +114,6 @@ def parse_args():
113114
help='Enable removing statements that are just a literal (including docstrings)',
114115
dest='remove_literal_statements',
115116
)
116-
minification_options.add_argument(
117-
'--no-remove-annotations',
118-
action='store_false',
119-
help='Disable removing function and variable annotations',
120-
dest='remove_annotations',
121-
)
122117
minification_options.add_argument(
123118
'--no-hoist-literals',
124119
action='store_false',
@@ -189,6 +184,39 @@ def parse_args():
189184
help='Replace explicit return None with a bare return',
190185
dest='remove_explicit_return_none',
191186
)
187+
188+
annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
189+
annotation_options.add_argument(
190+
'--no-remove-annotations',
191+
action='store_false',
192+
help='Disable removing all annotations',
193+
dest='remove_annotations',
194+
)
195+
annotation_options.add_argument(
196+
'--no-remove-variable-annotations',
197+
action='store_false',
198+
help='Disable removing variable annotations',
199+
dest='remove_variable_annotations',
200+
)
201+
annotation_options.add_argument(
202+
'--no-remove-return-annotations',
203+
action='store_false',
204+
help='Disable removing function return annotations',
205+
dest='remove_return_annotations',
206+
)
207+
annotation_options.add_argument(
208+
'--no-remove-argument-annotations',
209+
action='store_false',
210+
help='Disable removing function argument annotations',
211+
dest='remove_argument_annotations',
212+
)
213+
annotation_options.add_argument(
214+
'--remove-class-attribute-annotations',
215+
action='store_true',
216+
help='Enable removing class attribute annotations',
217+
dest='remove_class_attribute_annotations',
218+
)
219+
192220
parser.add_argument('--version', '-v', action='version', version=version)
193221

194222
args = parser.parse_args()
@@ -207,6 +235,10 @@ def parse_args():
207235
sys.stderr.write('error: path ' + args.path[0] + ' is a directory, --in-place required\n')
208236
sys.exit(1)
209237

238+
if args.remove_class_attribute_annotations and not args.remove_annotations:
239+
sys.stderr.write('error: --remove-class-attribute-annotations would do nothing when used with --no-remove-annotations\n')
240+
sys.exit(1)
241+
210242
return args
211243

212244
def source_modules(args):
@@ -237,12 +269,27 @@ def do_minify(source, filename, minification_args):
237269
names = [name.strip() for name in arg.split(',') if name]
238270
preserve_locals.extend(names)
239271

272+
if minification_args.remove_annotations is False:
273+
remove_annotations = RemoveAnnotationsOptions(
274+
remove_variable_annotations=False,
275+
remove_return_annotations=False,
276+
remove_argument_annotations=False,
277+
remove_class_attribute_annotations=False,
278+
)
279+
else:
280+
remove_annotations = RemoveAnnotationsOptions(
281+
remove_variable_annotations=minification_args.remove_variable_annotations,
282+
remove_return_annotations=minification_args.remove_return_annotations,
283+
remove_argument_annotations=minification_args.remove_argument_annotations,
284+
remove_class_attribute_annotations=minification_args.remove_class_attribute_annotations,
285+
)
286+
240287
return minify(
241288
source,
242289
filename=filename,
243290
combine_imports=minification_args.combine_imports,
244291
remove_pass=minification_args.remove_pass,
245-
remove_annotations=minification_args.remove_annotations,
292+
remove_annotations=remove_annotations,
246293
remove_literal_statements=minification_args.remove_literal_statements,
247294
hoist_literals=minification_args.hoist_literals,
248295
rename_locals=minification_args.rename_locals,

src/python_minifier/transforms/remove_annotations.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ast
22
import sys
33

4+
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
45
from python_minifier.transforms.suite_transformer import SuiteTransformer
56

67

@@ -9,6 +10,11 @@ class RemoveAnnotations(SuiteTransformer):
910
Remove type annotations from source
1011
"""
1112

13+
def __init__(self, options):
14+
assert isinstance(options, RemoveAnnotationsOptions)
15+
self._options = options
16+
super(RemoveAnnotations, self).__init__()
17+
1218
def __call__(self, node):
1319
if sys.version_info < (3, 0):
1420
return node
@@ -19,7 +25,7 @@ def visit_FunctionDef(self, node):
1925
node.body = self.suite(node.body, parent=node)
2026
node.decorator_list = [self.visit(d) for d in node.decorator_list]
2127

22-
if hasattr(node, 'returns'):
28+
if hasattr(node, 'returns') and self._options.remove_return_annotations:
2329
node.returns = None
2430

2531
return node
@@ -37,21 +43,24 @@ def visit_arguments(self, node):
3743
node.kwonlyargs = [self.visit_arg(a) for a in node.kwonlyargs]
3844

3945
if hasattr(node, 'varargannotation'):
40-
node.varargannotation = None
46+
if self._options.remove_argument_annotations:
47+
node.varargannotation = None
4148
else:
4249
if node.vararg:
4350
node.vararg = self.visit_arg(node.vararg)
4451

4552
if hasattr(node, 'kwargannotation'):
46-
node.kwargannotation = None
53+
if self._options.remove_argument_annotations:
54+
node.kwargannotation = None
4755
else:
4856
if node.kwarg:
4957
node.kwarg = self.visit_arg(node.kwarg)
5058

5159
return node
5260

5361
def visit_arg(self, node):
54-
node.annotation = None
62+
if self._options.remove_argument_annotations:
63+
node.annotation = None
5564
return node
5665

5766
def visit_AnnAssign(self, node):
@@ -97,6 +106,14 @@ def is_typing_sensitive(node):
97106

98107
return False
99108

109+
# is this a class attribute or a variable?
110+
if isinstance(node.parent, ast.ClassDef):
111+
if not self._options.remove_class_attribute_annotations:
112+
return node
113+
else:
114+
if not self._options.remove_variable_annotations:
115+
return node
116+
100117
if is_dataclass_field(node) or is_typing_sensitive(node):
101118
return node
102119
elif node.value:

0 commit comments

Comments
 (0)