Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ The following settings determine how different types should be evaluated.

- <a name="disableBytesTypePromotions"></a> **disableBytesTypePromotions** [boolean]: Disables legacy behavior where `bytearray` and `memoryview` are considered subtypes of `bytes`. [PEP 688](https://peps.python.org/pep-0688/#no-special-meaning-for-bytes) deprecates this behavior, but this switch is provided to restore the older behavior. The default value for this setting is `true`.

- <a name="noImplicitReexport"></a> **noImplicitReexport** [boolean]: PEP 484 indicates that imported symbols in `py.typed` packages are not considered re-exported unless they appear in `__all__` or use the redundant alias form (`from X import Y as Y`). When this setting is `false`, plain `from X import Y` imports are treated as valid re-exports for public names (those without a leading underscore), mirroring mypy's `no_implicit_reexport = false` behavior. Names with a leading underscore are always treated as private regardless of this setting. The default value for this setting is `true`.

## Type Check Diagnostics Settings
The following settings control pyright’s diagnostic output (warnings or errors).

Expand Down Expand Up @@ -351,6 +353,7 @@ The following table lists the default severity levels for each diagnostic rule w
| :---------------------------------------- | :--------- | :--------- | :--------- | :--------- |
| analyzeUnannotatedFunctions | true | true | true | true |
| disableBytesTypePromotions | true | true | true | true |
| noImplicitReexport | true | true | true | true |
| strictParameterNoneValue | true | true | true | true |
| enableTypeIgnoreComments | true | true | true | true |
| enableReachabilityAnalysis | false | true | true | true |
Expand Down
55 changes: 33 additions & 22 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5985,14 +5985,19 @@ export function createTypeEvaluator(
}

if (symbol.isPrivatePyTypedImport()) {
addDiagnostic(
DiagnosticRule.reportPrivateImportUsage,
LocMessage.privateImportFromPyTypedModule().format({
name: memberName,
module: baseType.priv.moduleName,
}),
node.d.member
);
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
const isImplicitReexportAllowed =
!fileInfo.diagnosticRuleSet.noImplicitReexport && !memberName.startsWith('_');
if (!isImplicitReexportAllowed) {
addDiagnostic(
DiagnosticRule.reportPrivateImportUsage,
LocMessage.privateImportFromPyTypedModule().format({
name: memberName,
module: baseType.priv.moduleName,
}),
node.d.member
);
}
}
} else {
// Does the module export a top-level __getattr__ function?
Expand Down Expand Up @@ -20499,22 +20504,28 @@ export function createTypeEvaluator(
}

if (resolvedAliasInfo.privatePyTypedImporter) {
const diag = new DiagnosticAddendum();
if (resolvedAliasInfo.privatePyTypedImported) {
diag.addMessage(
LocAddendum.privateImportFromPyTypedSource().format({
module: resolvedAliasInfo.privatePyTypedImported,
})
const importedName = node.d.name.d.value;
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
const isImplicitReexportAllowed =
!fileInfo.diagnosticRuleSet.noImplicitReexport && !importedName.startsWith('_');
if (!isImplicitReexportAllowed) {
const diag = new DiagnosticAddendum();
if (resolvedAliasInfo.privatePyTypedImported) {
diag.addMessage(
LocAddendum.privateImportFromPyTypedSource().format({
module: resolvedAliasInfo.privatePyTypedImported,
})
);
}
addDiagnostic(
DiagnosticRule.reportPrivateImportUsage,
LocMessage.privateImportFromPyTypedModule().format({
name: importedName,
module: resolvedAliasInfo.privatePyTypedImporter,
}) + diag.getString(),
node.d.name
);
}
addDiagnostic(
DiagnosticRule.reportPrivateImportUsage,
LocMessage.privateImportFromPyTypedModule().format({
name: node.d.name.d.value,
module: resolvedAliasInfo.privatePyTypedImporter,
}) + diag.getString(),
node.d.name
);
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/pyright-internal/src/common/configOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export interface DiagnosticRuleSet {
// No longer treat bytearray and memoryview as subclasses of bytes?
disableBytesTypePromotions: boolean;

// Treat plain `from X import Y` imports in py.typed packages as re-exports
// for public names? Mirrors mypy's no_implicit_reexport setting.
noImplicitReexport: boolean;

// Report general type issues?
reportGeneralTypeIssues: DiagnosticLevel;

Expand Down Expand Up @@ -417,6 +421,7 @@ export function getBooleanDiagnosticRules(includeNonOverridable = false) {
DiagnosticRule.enableExperimentalFeatures,
DiagnosticRule.deprecateTypingAliases,
DiagnosticRule.disableBytesTypePromotions,
DiagnosticRule.noImplicitReexport,
];

if (includeNonOverridable) {
Expand Down Expand Up @@ -541,6 +546,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: false,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
noImplicitReexport: true,
reportGeneralTypeIssues: 'none',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
Expand Down Expand Up @@ -644,6 +650,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
Expand Down Expand Up @@ -747,6 +754,7 @@ export function getStandardDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'error',
Expand Down Expand Up @@ -850,6 +858,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true, // Not overridden by strict mode
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'error',
Expand Down
1 change: 1 addition & 0 deletions packages/pyright-internal/src/common/diagnosticRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum DiagnosticRule {
enableReachabilityAnalysis = 'enableReachabilityAnalysis',
deprecateTypingAliases = 'deprecateTypingAliases',
disableBytesTypePromotions = 'disableBytesTypePromotions',
noImplicitReexport = 'noImplicitReexport',

reportGeneralTypeIssues = 'reportGeneralTypeIssues',
reportPropertyTypeMismatch = 'reportPropertyTypeMismatch',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// <reference path="typings/fourslash.d.ts" />

// @filename: pyrightconfig.json
//// {
//// "typeCheckingMode": "basic",
//// "noImplicitReexport": false
//// }

// @filename: testLib/py.typed
// @library: true
////

// @filename: testLib/__init__.py
// @library: true
//// from .module1 import one as one, two, three

// @filename: testLib/module1.py
// @library: true
//// one: int = 1
//// two: int = 2
//// three: int = 3

// @filename: .src/test1.py
//// # pyright: reportPrivateImportUsage=true
//// from testLib import one # explicit re-export (as-alias) — always ok
//// from testLib import two # plain import, public name — ok with noImplicitReexport=false
//// from testLib import three # plain import, public name — ok with noImplicitReexport=false
//// import testLib
//// testLib.one
//// testLib.two # ok with noImplicitReexport=false
//// testLib.three # ok with noImplicitReexport=false

// Verify that no reportPrivateImportUsage errors are raised for public names
// when noImplicitReexport=false. Private-name behavior is tested in privateImportUsage.test.ts.
// @ts-ignore
await helper.verifyDiagnostics();
102 changes: 102 additions & 0 deletions packages/pyright-internal/src/tests/privateImportUsage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,105 @@ describe('reportPrivateImportUsage with tracked library files', () => {
sp.dispose();
});
});

describe('noImplicitReexport config option', () => {
// pkg_a: py.typed library that re-exports PublicClass via plain import (no __all__, no `as` alias)
// pkg_b: consumer that imports PublicClass from pkg_a
const files = [
{
path: combinePaths(libraryRoot, 'pkg_a', '__init__.py'),
content: 'from ._impl import PublicClass\n_PrivateClass = object',
},
{
path: combinePaths(libraryRoot, 'pkg_a', 'py.typed'),
content: '',
},
{
path: combinePaths(libraryRoot, 'pkg_a', '_impl.py'),
content: 'class PublicClass: pass\nclass _PrivateClass: pass',
},
{
path: normalizeSlashes('/src/consumer.py'),
content: [
'from pkg_a import PublicClass', // public name — should be allowed with noImplicitReexport=false
'from pkg_a import _PrivateClass', // private name — should always error
].join('\n'),
},
];

test('public name implicit re-export should not error when noImplicitReexport=false', () => {
const sp = createServiceProviderFromFiles(files);
const configOptions = new ConfigOptions(UriEx.file('/'));
configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error';
configOptions.diagnosticRuleSet.noImplicitReexport = false;

const importResolver = new ImportResolver(
sp,
configOptions,
new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)])
);
const program = new Program(importResolver, configOptions, sp);
const consumerUri = UriEx.file('/src/consumer.py');
program.setTrackedFiles([consumerUri]);
while (program.analyze()) {
// keep analyzing until complete
}

const sourceFile = program.getSourceFile(consumerUri);
assert(sourceFile, 'Source file should exist');
const diagnostics = sourceFile.getDiagnostics(configOptions) || [];
const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error);

// PublicClass should not error; _PrivateClass should still error (1 total)
assert.strictEqual(
errors.length,
1,
`Expected 1 error (for _PrivateClass only), got ${errors.length}: ${errors
.map((e) => e.message)
.join(', ')}`
);
assert(
errors[0].message.includes('_PrivateClass'),
`The sole error should be about _PrivateClass, got: ${errors[0].message}`
);

program.dispose();
sp.dispose();
});

test('public name implicit re-export still errors when noImplicitReexport=true (default)', () => {
const sp = createServiceProviderFromFiles(files);
const configOptions = new ConfigOptions(UriEx.file('/'));
configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error';
configOptions.diagnosticRuleSet.noImplicitReexport = true;

const importResolver = new ImportResolver(
sp,
configOptions,
new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)])
);
const program = new Program(importResolver, configOptions, sp);
const consumerUri = UriEx.file('/src/consumer.py');
program.setTrackedFiles([consumerUri]);
while (program.analyze()) {
// keep analyzing until complete
}

const sourceFile = program.getSourceFile(consumerUri);
assert(sourceFile, 'Source file should exist');
const diagnostics = sourceFile.getDiagnostics(configOptions) || [];
const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error);

// Both imports should error when noImplicitReexport=true
assert.strictEqual(
errors.length,
2,
`Expected 2 errors (PublicClass and _PrivateClass), got ${errors.length}: ${errors
.map((e) => e.message)
.join(', ')}`
);

program.dispose();
sp.dispose();
});
});
6 changes: 6 additions & 0 deletions packages/vscode-pyright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,12 @@
"Trace"
]
},
"python.analysis.noImplicitReexport": {
"type": "boolean",
"default": true,
"description": "When true, imported symbols in py.typed packages require explicit re-export (via __all__ or 'as' alias). Set to false to allow plain imports as re-exports for public names.",
"scope": "resource"
},
"python.analysis.typeCheckingMode": {
"type": "string",
"default": "standard",
Expand Down
11 changes: 11 additions & 0 deletions packages/vscode-pyright/schemas/pyrightconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
"title": "Treat typing-specific aliases to standard types as deprecated",
"default": false
},
"noImplicitReexport": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you need a setting for this in the vscode-pyright package.json. Like the example here:

"reportGeneralTypeIssues": {

Copy link
Author

Choose a reason for hiding this comment

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

added in 1e6d878

Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't the title be the same for both?

Copy link
Author

Choose a reason for hiding this comment

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

yes, sorry--fixed now (and i've simplified the failing tests b/c I couldn't figure out why the strings weren't matching, they looked exactly the same)

"type": "boolean",
"title": "When true, imported symbols in py.typed packages require explicit re-export (via __all__ or 'as' alias). Set to false to allow plain imports as re-exports for public names.",
"default": true
},
"reportGeneralTypeIssues": {
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of general type issues",
Expand Down Expand Up @@ -618,6 +623,9 @@
"deprecateTypingAliases": {
"$ref": "#/definitions/deprecateTypingAliases"
},
"noImplicitReexport": {
"$ref": "#/definitions/noImplicitReexport"
},
"reportGeneralTypeIssues": {
"$ref": "#/definitions/reportGeneralTypeIssues"
},
Expand Down Expand Up @@ -936,6 +944,9 @@
"deprecateTypingAliases": {
"$ref": "#/definitions/deprecateTypingAliases"
},
"noImplicitReexport": {
"$ref": "#/definitions/noImplicitReexport"
},
"reportGeneralTypeIssues": {
"$ref": "#/definitions/reportGeneralTypeIssues"
},
Expand Down