diff --git a/Doc/library/__main__.rst b/Doc/library/__main__.rst index 4407ba2f7714dd..36b884f975804f 100644 --- a/Doc/library/__main__.rst +++ b/Doc/library/__main__.rst @@ -1,3 +1,5 @@ +.. _`__main__`: + :mod:`!__main__` --- Top-level code environment =============================================== diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 7af3457070b84a..fb40f16c1d76c2 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2911,6 +2911,8 @@ automatic property creation, proxies, frameworks, and automatic resource locking/synchronization. +.. _customize-instance-subclass-checks: + Customizing instance and subclass checks ---------------------------------------- diff --git a/Doc/tools/extensions/pydoc_topics.py b/Doc/tools/extensions/pydoc_topics.py index 01efbba628324f..df5d426db07b26 100644 --- a/Doc/tools/extensions/pydoc_topics.py +++ b/Doc/tools/extensions/pydoc_topics.py @@ -47,6 +47,7 @@ "continue", "conversions", "customization", + "customize-instance-subclass-checks", "debugger", "del", "dict", @@ -69,6 +70,7 @@ "integers", "lambda", "lists", + "name_equals_main", "naming", "nonlocal", "numbers", @@ -100,6 +102,7 @@ "while", "with", "yield", + "__main__", }) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d508fb70ea429e..a2d0882c4bbf50 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -1721,6 +1721,11 @@ def locate(path, forceload=0): object = getattr(object, part) except AttributeError: return None + if _is_dunder_name(path) and not isinstance(object, (type, type(__import__))): + # if we're looking up a special variable and we don't find a class or a + # function, it's probably not what the user wanted (if it is, they can + # look up builtins.whatever) + return None return object # --------------------------------------- interactive interpreter interface @@ -1734,10 +1739,15 @@ def resolve(thing, forceload=0): if isinstance(thing, str): object = locate(thing, forceload) if object is None: + if _is_dunder_name(thing): + special = "Use help('specialnames') for a list of special names for which help is available.\n" + else: + special = "" raise ImportError('''\ -No Python documentation found for %r. -Use help() to get the interactive help utility. -Use help(str) for help on the str class.''' % thing) +No help entry found for %r. +%sUse help() to get the interactive help utility. +Use help(str) for help on the str class. +Additional documentation is available online at https://docs.python.org/%s.%s/''' % (thing, special, *sys.version_info[:2])) return object, thing else: name = getattr(thing, '__name__', None) @@ -1827,10 +1837,10 @@ def _introdoc(): Python, you should definitely check out the tutorial at https://docs.python.org/{ver}/tutorial/. - Enter the name of any module, keyword, or topic to get help on writing - Python programs and using Python modules. To get a list of available - modules, keywords, symbols, or topics, enter "modules", "keywords", - "symbols", or "topics". + Enter the name of any module, keyword, symbol, or topic to get help on + writing Python programs and using Python modules. To get a list of + available modules, keywords, symbols, special names, or topics, enter + "modules", "keywords", "symbols", "specialnames", or "topics". {pyrepl_keys} Each module also comes with a one-line summary of what it does; to list the modules whose name or summary contain a given string such as "spam", @@ -1840,6 +1850,96 @@ def _introdoc(): enter "q", "quit" or "exit". ''') +def _is_dunder_name(x): + return isinstance(x, str) and len(x) > 4 and x[:2] == x[-2:] == '__' + +def collect_dunders(symbols): + dunders = { + '__name__': ('name_equals_main', ''), + '__main__': ('__main__', ''), + '__call__': ('callable-types', 'SPECIALMETHODS'), + } + + basic_dunders = [ + '__new__', '__init__', '__del__', '__repr__', '__str__', '__bytes__', + '__format__', '__hash__', '__bool__', + ] + for bd in basic_dunders: + dunders[bd] = ('customization', 'SPECIALMETHODS') + + attribute_dunders = [ + '__getattr__', '__getattribute__', '__setattr__', '__delattr__', + '__dir__', '__get__', '__set__', '__delete__', '__objclass__', + ] + for ad in attribute_dunders: + dunders[ad] = ('attribute-access', 'SPECIALMETHODS') + + class_dunders = [ + '__init_subclass__', '__set_names__', '__mro_entries__', + ] + for cd in class_dunders: + dunders[cd] = ('class-customization', 'SPECIALMETHODS') + + instance_dunders = [ + '__instancecheck__', '__subclasscheck__' + ] + for d in instance_dunders: + dunders[d] = ('customize-instance-subclass-checks', 'SPECIALMETHODS') + + sequence_dunders = [ + '__len__', '__length_hint__', '__getitem__', '__setitem__', + '__delitem__', '__missing__', '__iter__', '__reversed__', + '__contains__', + ] + for sd in sequence_dunders: + dunders[sd] = ('sequence-types', 'SPECIALMETHODS') + + comparison_dunders = { + '__lt__': '<', + '__le__': '<=', + '__eq__': '==', + '__ne__': '!=', + '__gt__': '>', + '__ge__': '>=', + } + for dunder, symbol in comparison_dunders.items(): + dunders[dunder] = ('customization', f'{symbol} SPECIALMETHODS') + if symbol in symbols: + symbols[symbol] += f' {dunder}' + + arithmetic_dunders = { + '__add__': '+', + '__sub__': '-', + '__mul__': '*', + '__truediv__': '/', + '__floordiv__': '//', + '__mod__': '%', + '__pow__': '**', + '__lshift__': '<<', + '__rshift__': '>>', + '__and__': '&', + '__or__': '|', + '__xor__': '^', + } + for dunder, symbol in arithmetic_dunders.items(): + rname = "__r" + dunder[2:] + iname = "__i" + dunder[2:] + dunders[dunder] = ('numeric-types', f'{symbol} {rname} {iname} SPECIALMETHODS') + dunders[rname] = ('numeric-types', f'{symbol} {dunder} SPECIALMETHODS') + dunders[iname] = ('numeric-types', f'{symbol} {dunder} SPECIALMETHODS') + if symbol in symbols: + symbols[symbol] += f' {dunder}' + + # __matmul__ isn't included above because help('@') doesn't talk about + # matrix multiplication, so we shouldn't list it here as a related topic. + dunders['__matmul__'] = ('numeric-types', f'__rmatmul__ __imatmul__ SPECIALMETHODS') + dunders['__rmatmul__'] = ('numeric-types', f'__matmul__ SPECIALMETHODS') + dunders['__imatmul__'] = ('numeric-types', f'__matmul__ SPECIALMETHODS') + + dunders['__divmod__'] = ('numeric-types', 'divmod') + + return dunders + class Helper: # These dictionaries map a topic name to either an alias, or a tuple @@ -1920,7 +2020,8 @@ class Helper: '(': 'TUPLES FUNCTIONS CALLS', ')': 'TUPLES FUNCTIONS CALLS', '[': 'LISTS SUBSCRIPTS SLICINGS', - ']': 'LISTS SUBSCRIPTS SLICINGS' + ']': 'LISTS SUBSCRIPTS SLICINGS', + } for topic, symbols_ in _symbols_inverse.items(): for symbol in symbols_: @@ -1970,8 +2071,7 @@ class Helper: 'BASICMETHODS': ('customization', 'hash repr str SPECIALMETHODS'), 'ATTRIBUTEMETHODS': ('attribute-access', 'ATTRIBUTES SPECIALMETHODS'), 'CALLABLEMETHODS': ('callable-types', 'CALLS SPECIALMETHODS'), - 'SEQUENCEMETHODS': ('sequence-types', 'SEQUENCES SEQUENCEMETHODS ' - 'SPECIALMETHODS'), + 'SEQUENCEMETHODS': ('sequence-types', 'SEQUENCES SPECIALMETHODS'), 'MAPPINGMETHODS': ('sequence-types', 'MAPPINGS SPECIALMETHODS'), 'NUMBERMETHODS': ('numeric-types', 'NUMBERS AUGMENTEDASSIGNMENT ' 'SPECIALMETHODS'), @@ -2016,8 +2116,15 @@ class Helper: 'TRUTHVALUE': ('truth', 'if while and or not BASICMETHODS'), 'DEBUGGING': ('debugger', 'pdb'), 'CONTEXTMANAGERS': ('context-managers', 'with'), + 'DUNDERMETHODS': 'SPECIALMETHODS', + 'MAINMODULE': '__main__', } + # add dunder methods + dunders = collect_dunders(symbols) + topics |= dunders + + def __init__(self, input=None, output=None): self._input = input self._output = output @@ -2090,6 +2197,8 @@ def help(self, request, is_cli=False): if request == 'keywords': self.listkeywords() elif request == 'symbols': self.listsymbols() elif request == 'topics': self.listtopics() + elif request == 'specialnames': + self.listdunders() elif request == 'modules': self.listmodules() elif request[:8] == 'modules ': self.listmodules(request.split()[1]) @@ -2141,7 +2250,14 @@ def listtopics(self): Here is a list of available topics. Enter any topic name to get more help. ''') - self.list(self.topics.keys(), columns=3) + self.list([k for k in self.topics if k not in self.dunders], columns=3) + + def listdunders(self): + self.output.write(''' +Here is a list of special names for which help is available. Enter any one to get more help. + +''') + self.list(self.dunders.keys(), columns=3) def showtopic(self, topic, more_xrefs=''): try: @@ -2845,7 +2961,8 @@ class BadUsage(Exception): pass reference to a class or function within a module or module in a package. If contains a '{sep}', it is used as the path to a Python source file to document. If name is 'keywords', 'topics', - or 'modules', a listing of these things is displayed. + 'symbols', 'specialnames', or 'modules', a listing of these things is + displayed. {cmd} -k Search for a keyword in the synopsis lines of all available modules. diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 5f7e14a79d3356..a6cd8d814bcf32 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -1,7 +1,357 @@ -# Autogenerated by Sphinx on Tue May 6 18:33:44 2025 +# Autogenerated by Sphinx on Wed Aug 20 02:20:40 2025 # as part of the release process. topics = { + '__main__': r'''"__main__" — Top-level code environment +*************************************** + +====================================================================== + +In Python, the special name "__main__" is used for two important +constructs: + +1. the name of the top-level environment of the program, which can be + checked using the "__name__ == '__main__'" expression; and + +2. the "__main__.py" file in Python packages. + +Both of these mechanisms are related to Python modules; how users +interact with them and how they interact with each other. They are +explained in detail below. If you’re new to Python modules, see the +tutorial section Modules for an introduction. + + +"__name__ == '__main__'" +======================== + +When a Python module or package is imported, "__name__" is set to the +module’s name. Usually, this is the name of the Python file itself +without the ".py" extension: + + >>> import configparser + >>> configparser.__name__ + 'configparser' + +If the file is part of a package, "__name__" will also include the +parent package’s path: + + >>> from concurrent.futures import process + >>> process.__name__ + 'concurrent.futures.process' + +However, if the module is executed in the top-level code environment, +its "__name__" is set to the string "'__main__'". + + +What is the “top-level code environment”? +----------------------------------------- + +"__main__" is the name of the environment where top-level code is run. +“Top-level code” is the first user-specified Python module that starts +running. It’s “top-level” because it imports all other modules that +the program needs. Sometimes “top-level code” is called an *entry +point* to the application. + +The top-level code environment can be: + +* the scope of an interactive prompt: + + >>> __name__ + '__main__' + +* the Python module passed to the Python interpreter as a file + argument: + + $ python helloworld.py + Hello, world! + +* the Python module or package passed to the Python interpreter with + the "-m" argument: + + $ python -m tarfile + usage: tarfile.py [-h] [-v] (...) + +* Python code read by the Python interpreter from standard input: + + $ echo "import this" | python + The Zen of Python, by Tim Peters + + Beautiful is better than ugly. + Explicit is better than implicit. + ... + +* Python code passed to the Python interpreter with the "-c" argument: + + $ python -c "import this" + The Zen of Python, by Tim Peters + + Beautiful is better than ugly. + Explicit is better than implicit. + ... + +In each of these situations, the top-level module’s "__name__" is set +to "'__main__'". + +As a result, a module can discover whether or not it is running in the +top-level environment by checking its own "__name__", which allows a +common idiom for conditionally executing code when the module is not +initialized from an import statement: + + if __name__ == '__main__': + # Execute when the module is not initialized from an import statement. + ... + +See also: + + For a more detailed look at how "__name__" is set in all situations, + see the tutorial section Modules. + + +Idiomatic Usage +--------------- + +Some modules contain code that is intended for script use only, like +parsing command-line arguments or fetching data from standard input. +If a module like this was imported from a different module, for +example to unit test it, the script code would unintentionally execute +as well. + +This is where using the "if __name__ == '__main__'" code block comes +in handy. Code within this block won’t run unless the module is +executed in the top-level environment. + +Putting as few statements as possible in the block below "if __name__ +== '__main__'" can improve code clarity and correctness. Most often, a +function named "main" encapsulates the program’s primary behavior: + + # echo.py + + import shlex + import sys + + def echo(phrase: str) -> None: + """A dummy wrapper around print.""" + # for demonstration purposes, you can imagine that there is some + # valuable and reusable logic inside this function + print(phrase) + + def main() -> int: + """Echo the input arguments to standard output""" + phrase = shlex.join(sys.argv) + echo(phrase) + return 0 + + if __name__ == '__main__': + sys.exit(main()) # next section explains the use of sys.exit + +Note that if the module didn’t encapsulate code inside the "main" +function but instead put it directly within the "if __name__ == +'__main__'" block, the "phrase" variable would be global to the entire +module. This is error-prone as other functions within the module +could be unintentionally using the global variable instead of a local +name. A "main" function solves this problem. + +Using a "main" function has the added benefit of the "echo" function +itself being isolated and importable elsewhere. When "echo.py" is +imported, the "echo" and "main" functions will be defined, but neither +of them will be called, because "__name__ != '__main__'". + + +Packaging Considerations +------------------------ + +"main" functions are often used to create command-line tools by +specifying them as entry points for console scripts. When this is +done, pip inserts the function call into a template script, where the +return value of "main" is passed into "sys.exit()". For example: + + sys.exit(main()) + +Since the call to "main" is wrapped in "sys.exit()", the expectation +is that your function will return some value acceptable as an input to +"sys.exit()"; typically, an integer or "None" (which is implicitly +returned if your function does not have a return statement). + +By proactively following this convention ourselves, our module will +have the same behavior when run directly (i.e. "python echo.py") as it +will have if we later package it as a console script entry-point in a +pip-installable package. + +In particular, be careful about returning strings from your "main" +function. "sys.exit()" will interpret a string argument as a failure +message, so your program will have an exit code of "1", indicating +failure, and the string will be written to "sys.stderr". The +"echo.py" example from earlier exemplifies using the +"sys.exit(main())" convention. + +See also: + + Python Packaging User Guide contains a collection of tutorials and + references on how to distribute and install Python packages with + modern tools. + + +"__main__.py" in Python Packages +================================ + +If you are not familiar with Python packages, see section Packages of +the tutorial. Most commonly, the "__main__.py" file is used to +provide a command-line interface for a package. Consider the following +hypothetical package, “bandclass”: + + bandclass + ├── __init__.py + ├── __main__.py + └── student.py + +"__main__.py" will be executed when the package itself is invoked +directly from the command line using the "-m" flag. For example: + + $ python -m bandclass + +This command will cause "__main__.py" to run. How you utilize this +mechanism will depend on the nature of the package you are writing, +but in this hypothetical case, it might make sense to allow the +teacher to search for students: + + # bandclass/__main__.py + + import sys + from .student import search_students + + student_name = sys.argv[1] if len(sys.argv) >= 2 else '' + print(f'Found student: {search_students(student_name)}') + +Note that "from .student import search_students" is an example of a +relative import. This import style can be used when referencing +modules within a package. For more details, see Intra-package +References in the Modules section of the tutorial. + + +Idiomatic Usage +--------------- + +The content of "__main__.py" typically isn’t fenced with an "if +__name__ == '__main__'" block. Instead, those files are kept short +and import functions to execute from other modules. Those other +modules can then be easily unit-tested and are properly reusable. + +If used, an "if __name__ == '__main__'" block will still work as +expected for a "__main__.py" file within a package, because its +"__name__" attribute will include the package’s path if imported: + + >>> import asyncio.__main__ + >>> asyncio.__main__.__name__ + 'asyncio.__main__' + +This won’t work for "__main__.py" files in the root directory of a +".zip" file though. Hence, for consistency, a minimal "__main__.py" +without a "__name__" check is preferred. + +See also: + + See "venv" for an example of a package with a minimal "__main__.py" + in the standard library. It doesn’t contain a "if __name__ == + '__main__'" block. You can invoke it with "python -m venv + [directory]". + + See "runpy" for more details on the "-m" flag to the interpreter + executable. + + See "zipapp" for how to run applications packaged as *.zip* files. + In this case Python looks for a "__main__.py" file in the root + directory of the archive. + + +"import __main__" +================= + +Regardless of which module a Python program was started with, other +modules running within that same program can import the top-level +environment’s scope (*namespace*) by importing the "__main__" module. +This doesn’t import a "__main__.py" file but rather whichever module +that received the special name "'__main__'". + +Here is an example module that consumes the "__main__" namespace: + + # namely.py + + import __main__ + + def did_user_define_their_name(): + return 'my_name' in dir(__main__) + + def print_user_name(): + if not did_user_define_their_name(): + raise ValueError('Define the variable `my_name`!') + + print(__main__.my_name) + +Example usage of this module could be as follows: + + # start.py + + import sys + + from namely import print_user_name + + # my_name = "Dinsdale" + + def main(): + try: + print_user_name() + except ValueError as ve: + return str(ve) + + if __name__ == "__main__": + sys.exit(main()) + +Now, if we started our program, the result would look like this: + + $ python start.py + Define the variable `my_name`! + +The exit code of the program would be 1, indicating an error. +Uncommenting the line with "my_name = "Dinsdale"" fixes the program +and now it exits with status code 0, indicating success: + + $ python start.py + Dinsdale + +Note that importing "__main__" doesn’t cause any issues with +unintentionally running top-level code meant for script use which is +put in the "if __name__ == "__main__"" block of the "start" module. +Why does this work? + +Python inserts an empty "__main__" module in "sys.modules" at +interpreter startup, and populates it by running top-level code. In +our example this is the "start" module which runs line by line and +imports "namely". In turn, "namely" imports "__main__" (which is +really "start"). That’s an import cycle! Fortunately, since the +partially populated "__main__" module is present in "sys.modules", +Python passes that to "namely". See Special considerations for +__main__ in the import system’s reference for details on how this +works. + +The Python REPL is another example of a “top-level environment”, so +anything defined in the REPL becomes part of the "__main__" scope: + + >>> import namely + >>> namely.did_user_define_their_name() + False + >>> namely.print_user_name() + Traceback (most recent call last): + ... + ValueError: Define the variable `my_name`! + >>> my_name = 'Jabberwocky' + >>> namely.did_user_define_their_name() + True + >>> namely.print_user_name() + Jabberwocky + +The "__main__" scope is used in the implementation of "pdb" and +"rlcompleter". +''', 'assert': r'''The "assert" statement ********************** @@ -3764,6 +4114,43 @@ def __hash__(self): considered true if its result is nonzero. If a class defines neither "__len__()" nor "__bool__()" (which is true of the "object" class itself), all its instances are considered true. +''', + 'customize-instance-subclass-checks': r'''Customizing instance and subclass checks +**************************************** + +The following methods are used to override the default behavior of the +"isinstance()" and "issubclass()" built-in functions. + +In particular, the metaclass "abc.ABCMeta" implements these methods in +order to allow the addition of Abstract Base Classes (ABCs) as +“virtual base classes” to any class or type (including built-in +types), including other ABCs. + +type.__instancecheck__(self, instance) + + Return true if *instance* should be considered a (direct or + indirect) instance of *class*. If defined, called to implement + "isinstance(instance, class)". + +type.__subclasscheck__(self, subclass) + + Return true if *subclass* should be considered a (direct or + indirect) subclass of *class*. If defined, called to implement + "issubclass(subclass, class)". + +Note that these methods are looked up on the type (metaclass) of a +class. They cannot be defined as class methods in the actual class. +This is consistent with the lookup of special methods that are called +on instances, only in this case the instance is itself a class. + +See also: + + **PEP 3119** - Introducing Abstract Base Classes + Includes the specification for customizing "isinstance()" and + "issubclass()" behavior through "__instancecheck__()" and + "__subclasscheck__()", with motivation for this functionality in + the context of adding Abstract Base Classes (see the "abc" + module) to the language. ''', 'debugger': r'''"pdb" — The Python Debugger *************************** @@ -6418,6 +6805,175 @@ def (parameters): from left to right and placed into the list object in that order. When a comprehension is supplied, the list is constructed from the elements resulting from the comprehension. +''', + 'name_equals_main': r'''"__name__ == '__main__'" +************************ + +When a Python module or package is imported, "__name__" is set to the +module’s name. Usually, this is the name of the Python file itself +without the ".py" extension: + + >>> import configparser + >>> configparser.__name__ + 'configparser' + +If the file is part of a package, "__name__" will also include the +parent package’s path: + + >>> from concurrent.futures import process + >>> process.__name__ + 'concurrent.futures.process' + +However, if the module is executed in the top-level code environment, +its "__name__" is set to the string "'__main__'". + + +What is the “top-level code environment”? +========================================= + +"__main__" is the name of the environment where top-level code is run. +“Top-level code” is the first user-specified Python module that starts +running. It’s “top-level” because it imports all other modules that +the program needs. Sometimes “top-level code” is called an *entry +point* to the application. + +The top-level code environment can be: + +* the scope of an interactive prompt: + + >>> __name__ + '__main__' + +* the Python module passed to the Python interpreter as a file + argument: + + $ python helloworld.py + Hello, world! + +* the Python module or package passed to the Python interpreter with + the "-m" argument: + + $ python -m tarfile + usage: tarfile.py [-h] [-v] (...) + +* Python code read by the Python interpreter from standard input: + + $ echo "import this" | python + The Zen of Python, by Tim Peters + + Beautiful is better than ugly. + Explicit is better than implicit. + ... + +* Python code passed to the Python interpreter with the "-c" argument: + + $ python -c "import this" + The Zen of Python, by Tim Peters + + Beautiful is better than ugly. + Explicit is better than implicit. + ... + +In each of these situations, the top-level module’s "__name__" is set +to "'__main__'". + +As a result, a module can discover whether or not it is running in the +top-level environment by checking its own "__name__", which allows a +common idiom for conditionally executing code when the module is not +initialized from an import statement: + + if __name__ == '__main__': + # Execute when the module is not initialized from an import statement. + ... + +See also: + + For a more detailed look at how "__name__" is set in all situations, + see the tutorial section Modules. + + +Idiomatic Usage +=============== + +Some modules contain code that is intended for script use only, like +parsing command-line arguments or fetching data from standard input. +If a module like this was imported from a different module, for +example to unit test it, the script code would unintentionally execute +as well. + +This is where using the "if __name__ == '__main__'" code block comes +in handy. Code within this block won’t run unless the module is +executed in the top-level environment. + +Putting as few statements as possible in the block below "if __name__ +== '__main__'" can improve code clarity and correctness. Most often, a +function named "main" encapsulates the program’s primary behavior: + + # echo.py + + import shlex + import sys + + def echo(phrase: str) -> None: + """A dummy wrapper around print.""" + # for demonstration purposes, you can imagine that there is some + # valuable and reusable logic inside this function + print(phrase) + + def main() -> int: + """Echo the input arguments to standard output""" + phrase = shlex.join(sys.argv) + echo(phrase) + return 0 + + if __name__ == '__main__': + sys.exit(main()) # next section explains the use of sys.exit + +Note that if the module didn’t encapsulate code inside the "main" +function but instead put it directly within the "if __name__ == +'__main__'" block, the "phrase" variable would be global to the entire +module. This is error-prone as other functions within the module +could be unintentionally using the global variable instead of a local +name. A "main" function solves this problem. + +Using a "main" function has the added benefit of the "echo" function +itself being isolated and importable elsewhere. When "echo.py" is +imported, the "echo" and "main" functions will be defined, but neither +of them will be called, because "__name__ != '__main__'". + + +Packaging Considerations +======================== + +"main" functions are often used to create command-line tools by +specifying them as entry points for console scripts. When this is +done, pip inserts the function call into a template script, where the +return value of "main" is passed into "sys.exit()". For example: + + sys.exit(main()) + +Since the call to "main" is wrapped in "sys.exit()", the expectation +is that your function will return some value acceptable as an input to +"sys.exit()"; typically, an integer or "None" (which is implicitly +returned if your function does not have a return statement). + +By proactively following this convention ourselves, our module will +have the same behavior when run directly (i.e. "python echo.py") as it +will have if we later package it as a console script entry-point in a +pip-installable package. + +In particular, be careful about returning strings from your "main" +function. "sys.exit()" will interpret a string argument as a failure +message, so your program will have an exit code of "1", indicating +failure, and the string will be written to "sys.stderr". The +"echo.py" example from earlier exemplifies using the +"sys.exit(main())" convention. + +See also: + + Python Packaging User Guide contains a collection of tutorials and + references on how to distribute and install Python packages with + modern tools. ''', 'naming': r'''Naming and binding ****************** diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 3b50ead00bdd31..dc4710eba009bc 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -229,10 +229,13 @@ class C(builtins.object) for s in expected_data_docstrings) # output pattern for missing module -missing_pattern = '''\ -No Python documentation found for %r. -Use help() to get the interactive help utility. -Use help(str) for help on the str class.'''.replace('\n', os.linesep) +def missing_pattern(name, dunder=False): + dunderhelp = "Use help('specialnames') for a list of special names for which help is available.\n" if dunder else "" + return ('''\ +No help entry found for %r. +%sUse help() to get the interactive help utility. +Use help(str) for help on the str class. +Additional documentation is available online at https://docs.python.org/%s.%s/''' % (name, dunderhelp, *sys.version_info[:2])).replace('\n', os.linesep) # output pattern for module with bad imports badimport_pattern = "problem in %s - ModuleNotFoundError: No module named %r" @@ -497,10 +500,34 @@ class B: def test_not_here(self): missing_module = "test.i_am_not_here" result = str(run_pydoc_fail(missing_module), 'ascii') - expected = missing_pattern % missing_module + expected = missing_pattern(missing_module) self.assertEqual(expected, result, "documentation for missing module found") + def test_dunder_help(self): + def get_main_output(topic): + return run_pydoc(topic).split(b'Related help topics:')[0].strip() + + # check that each dunder method maps to a help topic that includes its + # name + for name in pydoc.Helper.dunders: + self.assertIn(name.encode('ascii'), get_main_output(name)) + + # check that right-hand and in-place versions match + self.assertEqual(get_main_output('__add__'), get_main_output('__radd__')) + self.assertEqual(get_main_output('__add__'), get_main_output('__iadd__')) + + def test_dunder_main(self): + self.assertEqual(run_pydoc('__main__').splitlines(False)[0], + '"__main__" — Top-level code environment'.encode('utf-8')) + self.assertEqual(run_pydoc('__name__').splitlines(False)[0], + '''"__name__ == '__main__'"'''.encode('utf-8')) + + def test_dunder_not_here(self): + result = str(run_pydoc_fail("__dict__"), 'ascii') + expected = missing_pattern("__dict__", dunder=True) + self.assertEqual(expected, result) + @requires_docstrings def test_not_ascii(self): result = run_pydoc('test.test_pydoc.test_pydoc.nonascii', PYTHONIOENCODING='ascii') @@ -510,7 +537,7 @@ def test_not_ascii(self): def test_input_strip(self): missing_module = " test.i_am_not_here " result = str(run_pydoc_fail(missing_module), 'ascii') - expected = missing_pattern % missing_module.strip() + expected = missing_pattern(missing_module.strip()) self.assertEqual(expected, result) def test_stripid(self): @@ -664,10 +691,10 @@ def test_builtin_on_metaclasses(self): self.assertNotIn('Built-in subclasses', text) def test_fail_help_cli(self): - elines = (missing_pattern % 'abd').splitlines() + elines = (missing_pattern("abd")).splitlines() with spawn_python("-c" "help()") as proc: out, _ = proc.communicate(b"abd") - olines = out.decode().splitlines()[-9:-6] + olines = out.decode().splitlines()[-10:-6] olines[0] = olines[0].removeprefix('help> ') self.assertEqual(elines, olines) @@ -675,7 +702,7 @@ def test_fail_help_output_redirect(self): with StringIO() as buf: helper = pydoc.Helper(output=buf) helper.help("abd") - expected = missing_pattern % "abd" + expected = missing_pattern("abd") self.assertEqual(expected, buf.getvalue().strip().replace('\n', os.linesep)) @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(), @@ -737,6 +764,8 @@ def run_pydoc_for_request(request, expected_text_part): run_pydoc_for_request('keywords', 'Here is a list of the Python keywords.') # test for "symbols" run_pydoc_for_request('symbols', 'Here is a list of the punctuation symbols') + # test for "specialnames" + run_pydoc_for_request('specialnames', 'Here is a list of special names') # test for "topics" run_pydoc_for_request('topics', 'Here is a list of available topics.') # test for "modules" skipped, see test_modules() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-19-16-10-26.gh-issue-137966.nEgFAt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-19-16-10-26.gh-issue-137966.nEgFAt.rst new file mode 100644 index 00000000000000..0d37c6b2992c12 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-19-16-10-26.gh-issue-137966.nEgFAt.rst @@ -0,0 +1,2 @@ +Add support for additional special names to the built-in ``help`` function. +Patch by Adam Hartz.