Skip to content

Commit d4eefe8

Browse files
SONARPY-961 Typeshed: serialize only public import
1 parent 783524b commit d4eefe8

File tree

8 files changed

+72
-47
lines changed

8 files changed

+72
-47
lines changed

python-frontend/src/test/java/org/sonar/python/types/TypeShedTest.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,18 @@ public void package_inner_submodules_symbols() {
195195
@Test
196196
public void package_relative_import() {
197197
Map<String, Symbol> osSymbols = symbolsForModule("os");
198-
Symbol sysSymbol = osSymbols.get("sys");
199-
assertThat(sysSymbol.kind()).isEqualTo(Kind.OTHER);
200-
Set<String> sysExportedSymbols = symbolsForModule("sys").keySet();
201-
assertThat(((SymbolImpl) sysSymbol).getChildrenSymbolByName().values()).extracting(Symbol::name).containsAll(sysExportedSymbols);
202-
203-
Symbol timesResult = osSymbols.get("times_result");
204-
assertThat(timesResult.kind()).isEqualTo(Kind.CLASS);
205-
assertThat(timesResult.fullyQualifiedName()).isEqualTo("os.times_result");
198+
// The "import sys" is not part of the exported API (private import) in Typeshed
199+
// See: https://github.com/python/typeshed/blob/master/CONTRIBUTING.md#conventions
200+
assertThat(osSymbols).doesNotContainKey("sys");
201+
202+
Map<String, Symbol> sqlite3Symbols = symbolsForModule("sqlite3");
203+
Symbol completeStatementFunction = sqlite3Symbols.get("complete_statement");
204+
assertThat(completeStatementFunction.kind()).isEqualTo(Kind.FUNCTION);
205+
assertThat(completeStatementFunction.fullyQualifiedName()).isEqualTo("sqlite3.dbapi2.complete_statement");
206+
Set<String> sqlite3Dbapi2Symbols = symbolsForModule("sqlite3.dbapi2").keySet();
207+
// Python names with a leading underscore are not imported when using wildcard imports
208+
sqlite3Dbapi2Symbols.removeIf(s -> s.startsWith("_"));
209+
assertThat(sqlite3Symbols.keySet()).containsAll(sqlite3Dbapi2Symbols);
206210

207211
Map<String, Symbol> requestsSymbols = symbolsForModule("requests");
208212
Symbol requestSymbol = requestsSymbols.get("request");
@@ -296,8 +300,7 @@ public void deserialize_annoy_protobuf() {
296300
Map<String, Symbol> deserializedAnnoySymbols = symbolsForModule("annoy").values().stream()
297301
.collect(Collectors.toMap(Symbol::fullyQualifiedName, s -> s));
298302
assertThat(deserializedAnnoySymbols.values()).extracting(Symbol::kind, Symbol::fullyQualifiedName)
299-
.containsExactlyInAnyOrder(tuple(Kind.CLASS, "annoy._Vector"), tuple(Kind.CLASS, "annoy.AnnoyIndex"), tuple(Kind.OTHER, "annoy.Literal"),
300-
tuple(Kind.CLASS, "annoy.Sized"), tuple(Kind.FUNCTION, "annoy.overload"), tuple(Kind.OTHER, "annoy.Protocol"));
303+
.containsExactlyInAnyOrder(tuple(Kind.CLASS, "annoy._Vector"), tuple(Kind.CLASS, "annoy.AnnoyIndex"));
301304

302305
ClassSymbol vector = (ClassSymbol) deserializedAnnoySymbols.get("annoy._Vector");
303306
assertThat(vector.superClasses()).extracting(Symbol::kind, Symbol::fullyQualifiedName)

python-frontend/typeshed_serializer/serializer/symbols.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,21 @@ def __init__(self, mypy_file: mpn.MypyFile):
367367
self.functions = []
368368
self.overloaded_functions = []
369369
self.vars = []
370+
private_imports = set()
371+
for elem in mypy_file.imports:
372+
# imports without aliases are considered private in Typeshed convention
373+
if isinstance(elem, mpn.Import):
374+
for _id, alias in elem.ids:
375+
if _id != alias:
376+
private_imports.add(_id)
377+
if isinstance(elem, mpn.ImportFrom):
378+
for _id, alias in elem.names:
379+
if _id != alias:
380+
private_imports.add(_id)
370381
for key in mypy_file.names:
371382
name = mypy_file.names.get(key)
383+
if key in private_imports and not name.fullname.startswith(mypy_file.fullname):
384+
continue
372385
if name.fullname == SONAR_CUSTOM_BASE_CLASS:
373386
# Ignore custom stub name
374387
continue

python-frontend/typeshed_serializer/serializer/typeshed_serializer.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,6 @@ def get_options(python_version=(3, 8)):
4141
return opt
4242

4343

44-
def build_single_module(module_fqn: str, category="stdlib", python_version=(3, 8)):
45-
opt = get_options(python_version)
46-
module_source = load_single_module(module_fqn, category)
47-
build_result = build.build([module_source], opt)
48-
built_file = build_result.files.get(module_fqn)
49-
return built_file
50-
51-
52-
def load_single_module(module_fqn: str, category="stdlib"):
53-
module_path = module_fqn
54-
if '.' in module_fqn:
55-
module_path = module_fqn.replace('.', "/")
56-
if os.path.isfile(path := os.path.join(CURRENT_PATH,
57-
f"../resources/typeshed/{category}/{module_path}.pyi")):
58-
module_source = build.BuildSource(path, module_fqn)
59-
elif os.path.isfile(path := os.path.join(CURRENT_PATH,
60-
f"../resources/typeshed/{category}/{module_path}/__init__.pyi")):
61-
module_source = build.BuildSource(path, module_fqn)
62-
else:
63-
raise FileNotFoundError(f"No stub found for module {module_fqn}")
64-
return module_source
65-
66-
6744
def walk_typeshed_stdlib(opt: options.Options = get_options()):
6845
generate_python2_stdlib = opt.python_version < (3, 0)
6946
relative_path = STDLIB_PATH if not generate_python2_stdlib else f"{STDLIB_PATH}/@python2"

python-frontend/typeshed_serializer/tests/conftest.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
#
2020

2121
import os
22-
from unittest.mock import Mock
2322

2423
import pytest
2524
from mypy import build
2625

2726
from serializer import typeshed_serializer, symbols_merger
27+
from serializer.typeshed_serializer import get_options
28+
29+
CURRENT_PATH = os.path.dirname(__file__)
2830

2931

3032
@pytest.fixture(scope="session")
@@ -42,13 +44,29 @@ def typeshed_custom_stubs():
4244

4345
@pytest.fixture(scope="session")
4446
def fake_module_36_38():
45-
fake_module_path = os.path.join(os.path.dirname(__file__), "resources/fakemodule.pyi")
46-
typeshed_serializer.load_single_module = Mock(return_value=build.BuildSource(fake_module_path, "fakemodule"))
47-
fake_module_36 = typeshed_serializer.build_single_module('fakemodule', python_version=(3, 6))
48-
fake_module_38 = typeshed_serializer.build_single_module('fakemodule', python_version=(3, 8))
49-
return [fake_module_36, fake_module_38]
47+
modules = {
48+
"fakemodule": os.path.join(CURRENT_PATH, "resources/fakemodule.pyi"),
49+
"fakemodule_imported": os.path.join(CURRENT_PATH, "resources/fakemodule_imported.pyi")
50+
}
51+
model_36 = build_modules(modules, python_version=(3, 6))
52+
model_38 = build_modules(modules, python_version=(3, 8))
53+
return [model_36.get("fakemodule"), model_38.get("fakemodule")]
5054

5155

5256
@pytest.fixture(scope="session")
5357
def typeshed_third_parties():
5458
return symbols_merger.merge_multiple_python_versions(is_third_parties=True)
59+
60+
61+
def build_modules(modules: dict[str, str], python_version=(3, 8)):
62+
opt = get_options(python_version)
63+
module_sources = []
64+
for module_fqn in modules.keys():
65+
module_sources.append(load_single_module(modules.get(module_fqn), module_fqn))
66+
build_result = build.build(module_sources, opt)
67+
return build_result.files
68+
69+
70+
def load_single_module(module_path: str, module_fqn):
71+
module_source = build.BuildSource(module_path, module_fqn)
72+
return module_source

python-frontend/typeshed_serializer/tests/resources/fakemodule.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
2+
import math as math
23
from typing import overload
34
from sys import flags as my_flags
5+
from fakemodule_imported import *
46

57
if sys.version_info >= (3, 8):
68
class SomeClassUnique38:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def common_imported_func():
2+
...
3+
4+
5+
def _private_func():
6+
...

python-frontend/typeshed_serializer/tests/test_symbols.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ def test_module_symbol(typeshed_stdlib):
3030
abc_module = typeshed_stdlib.files.get("abc")
3131
module_symbol = symbols.ModuleSymbol(abc_module)
3232
assert module_symbol.fullname == "abc"
33-
assert len(module_symbol.classes) == 5
33+
assert len(module_symbol.classes) == 3
3434
assert len(module_symbol.functions) == 4
3535

3636
pb_module = module_symbol.to_proto()
3737
assert pb_module.fully_qualified_name == "abc"
38-
assert len(pb_module.classes) == 5
38+
assert len(pb_module.classes) == 3
3939
assert len(pb_module.functions) == 4
4040
imported_modules = [imported_module for imported_module in pb_module.vars if
4141
imported_module.is_imported_module is True]
@@ -45,9 +45,8 @@ def test_module_symbol(typeshed_stdlib):
4545
pb_module = symbols.ModuleSymbol(os_module).to_proto()
4646
imported_modules = [imported_module for imported_module in pb_module.vars if
4747
imported_module.is_imported_module is True]
48-
assert len(imported_modules) == 3
48+
assert len(imported_modules) == 2
4949
imported_modules = map(lambda m: (m.fully_qualified_name, m.name), imported_modules)
50-
assert ("sys", "sys") in imported_modules
5150
assert ("os.path", "_path") in imported_modules
5251
assert ("os.path", "path") in imported_modules
5352

python-frontend/typeshed_serializer/tests/test_symbols_merger.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,17 @@ def test_actual_module_merge(fake_module_36_38):
168168

169169
functions_dict = merged_fakemodule_module.functions
170170
assert len(functions_dict) == 5
171+
171172
common_function_symbols = functions_dict['fakemodule.common_function']
172173
assert len(common_function_symbols) == 1
173174
assert common_function_symbols[0].valid_for == ["36", "38"]
174175

176+
common_function_wildcard_imported = functions_dict['fakemodule_imported.common_imported_func']
177+
assert len(common_function_wildcard_imported) == 1
178+
assert common_function_wildcard_imported[0].valid_for == ["36", "38"]
179+
180+
assert 'fakemodule_imported._private_func' not in functions_dict
181+
175182
function_unique_36 = functions_dict['fakemodule.function_unique_36']
176183
assert len(function_unique_36) == 1
177184
assert function_unique_36[0].valid_for == ["36"]
@@ -239,9 +246,9 @@ def test_actual_module_merge(fake_module_36_38):
239246
assert alias_symbol.type.kind == TypeKind.CALLABLE
240247
assert alias_symbol.type.pretty_printed_name == "CallableType[builtins.function]"
241248

242-
imported_sys = all_vars['sys']
243-
assert len(imported_sys) == 1
244-
assert imported_sys[0].var_symbol.is_imported_module is True
249+
imported_math = all_vars['math']
250+
assert len(imported_math) == 1
251+
assert imported_math[0].var_symbol.is_imported_module is True
245252

246253
fakemodule_class_with_fields_symbols = classes_dict['fakemodule.ClassWithFields']
247254
assert len(fakemodule_class_with_fields_symbols) == 1
@@ -317,7 +324,7 @@ def assert_abc_merged_module(merged_modules, expected_valid_for):
317324
assert isinstance(abc_merged_symbol, MergedModuleSymbol)
318325
assert abc_merged_symbol.fullname == "abc"
319326
assert ([c for c in abc_merged_symbol.classes]
320-
== ['_typeshed.SupportsWrite', 'typing.TypeVar', 'abc.ABCMeta', 'abc.abstractproperty', 'abc.ABC'])
327+
== ['abc.ABCMeta', 'abc.abstractproperty', 'abc.ABC'])
321328
for merged_class_proto in abc_merged_symbol.classes.values():
322329
assert len(merged_class_proto) == 1
323330
assert merged_class_proto[0].valid_for == expected_valid_for

0 commit comments

Comments
 (0)