Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-33826: add __filename__ to Python-defined classes for introspection #13894

Closed
wants to merge 1 commit into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion Doc/reference/datamodel.rst
Expand Up @@ -788,6 +788,7 @@ Custom classes
single: __bases__ (class attribute)
single: __doc__ (class attribute)
single: __annotations__ (class attribute)
single: __filename__ (class attribute)

Special attributes: :attr:`~definition.__name__` is the class name; :attr:`__module__` is
the module name in which the class was defined; :attr:`~object.__dict__` is the
Expand All @@ -796,7 +797,7 @@ Custom classes
base class list; :attr:`__doc__` is the class's documentation string,
or ``None`` if undefined; :attr:`__annotations__` (optional) is a dictionary
containing :term:`variable annotations <variable annotation>` collected during
class body execution.
class body execution; :attr:`__filename__` is the filename in which the class was defined. It may be missing, e.g., for built-in or C-defined classes.

Class instances
.. index::
Expand Down
2 changes: 2 additions & 0 deletions Lib/inspect.py
Expand Up @@ -658,6 +658,8 @@ def getfile(object):
return object.__file__
raise TypeError('{!r} is a built-in module'.format(object))
if isclass(object):
if hasattr(object, '__filename__'):
return object.__filename__
if hasattr(object, '__module__'):
object = sys.modules.get(object.__module__)
if getattr(object, '__file__', None):
Expand Down
10 changes: 5 additions & 5 deletions Lib/test/test_descr.py
Expand Up @@ -4844,8 +4844,8 @@ def test_iter_keys(self):
self.assertNotIsInstance(it, list)
keys = list(it)
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
'__weakref__', 'meth'])
self.assertEqual(keys, ['__dict__', '__doc__', '__filename__',
'__module__', '__weakref__', 'meth'])

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__')
Expand All @@ -4854,7 +4854,7 @@ def test_iter_values(self):
it = self.C.__dict__.values()
self.assertNotIsInstance(it, list)
values = list(it)
self.assertEqual(len(values), 5)
self.assertEqual(len(values), 6)

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__')
Expand All @@ -4864,8 +4864,8 @@ def test_iter_items(self):
self.assertNotIsInstance(it, list)
keys = [item[0] for item in it]
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
'__weakref__', 'meth'])
self.assertEqual(keys, ['__dict__', '__doc__', '__filename__',
'__module__', '__weakref__', 'meth'])

def test_dict_type_with_metaclass(self):
# Testing type of __dict__ when metaclass set...
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_inspect.py
Expand Up @@ -520,8 +520,7 @@ def __module__(cls):
raise AttributeError
class C(metaclass=CM):
pass
with self.assertRaises(TypeError):
inspect.getfile(C)
self.assertEqual(inspect.getfile(C), __file__)

def test_getfile_broken_repr(self):
class ErrorRepr:
Expand Down
7 changes: 4 additions & 3 deletions Lib/test/test_metaclass.py
Expand Up @@ -145,7 +145,8 @@

>>> class LoggingDict(dict):
... def __setitem__(self, key, value):
... print("d[%r] = %r" % (key, value))
... if key != '__filename__':
... print("d[%r] = %r" % (key, value))
... dict.__setitem__(self, key, value)
...
>>> class Meta(type):
Expand All @@ -169,7 +170,7 @@

>>> def meta(name, bases, namespace, **kwds):
... print("meta:", name, bases)
... print("ns:", sorted(namespace.items()))
... print("ns:", sorted([(k, v) for k, v in namespace.items() if k != "__filename__"]))
... print("kw:", sorted(kwds.items()))
... return namespace
...
Expand All @@ -182,7 +183,7 @@
kw: []
>>> type(C) is dict
True
>>> print(sorted(C.items()))
>>> print(sorted([(k, v) for k, v in C.items() if k != "__filename__"]))
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
>>>

Expand Down
63 changes: 54 additions & 9 deletions Lib/test/test_pydoc.py
Expand Up @@ -41,9 +41,9 @@ class nonascii:
expected_data_docstrings = (
'dictionary for instance variables (if defined)',
'list of weak references to the object (if defined)',
) * 2
)
else:
expected_data_docstrings = ('', '', '', '')
expected_data_docstrings = ('', '')

expected_text_pattern = """
NAME
Expand All @@ -69,6 +69,11 @@ class A(builtins.object)
| __dict__%s
|\x20\x20
| __weakref__%s
|\x20\x20
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|\x20\x20
| __filename__ = '%s'
\x20\x20\x20\x20
class B(builtins.object)
| Data descriptors defined here:
Expand All @@ -83,6 +88,8 @@ class B(builtins.object)
| NO_MEANING = 'eggs'
|\x20\x20
| __annotations__ = {'NO_MEANING': <class 'str'>}
|\x20\x20
| __filename__ = '%s'
\x20\x20\x20\x20
class C(builtins.object)
| Methods defined here:
Expand All @@ -103,6 +110,11 @@ class C(builtins.object)
|\x20\x20
| __weakref__
| list of weak references to the object (if defined)
|\x20\x20
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|\x20\x20
| __filename__ = '%s'

FUNCTIONS
doc_func()
Expand Down Expand Up @@ -177,6 +189,10 @@ class C(builtins.object)
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>%s</tt></dd>
</dl>
<hr>
Data and other attributes defined here:<br>
<dl><dt><strong>__filename__</strong> = '%s'</dl>

</td></tr></table> <p>
<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
Expand All @@ -197,6 +213,8 @@ class C(builtins.object)

<dl><dt><strong>__annotations__</strong> = {'NO_MEANING': &lt;class 'str'&gt;}</dl>

<dl><dt><strong>__filename__</strong> = '%s'</dl>

</td></tr></table> <p>
<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
Expand All @@ -219,6 +237,10 @@ class C(builtins.object)
<dl><dt><strong>__weakref__</strong></dt>
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
<hr>
Data and other attributes defined here:<br>
<dl><dt><strong>__filename__</strong> = '%s'</dl>

</td></tr></table></td></tr></table><p>
<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#eeaa77">
Expand Down Expand Up @@ -280,6 +302,11 @@ class DA(builtins.object)
| ham
|\x20\x20
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|\x20\x20
| __filename__ = '%s'
|\x20\x20
| ----------------------------------------------------------------------
| Data and other attributes inherited from Meta:
|\x20\x20
| ham = 'spam'
Expand Down Expand Up @@ -430,7 +457,11 @@ def test_html_doc(self):
mod_url = urllib.parse.quote(mod_file)
expected_html = expected_html_pattern % (
(mod_url, mod_file, doc_loc) +
expected_html_data_docstrings)
expected_html_data_docstrings +
(mod_file,) +
expected_html_data_docstrings +
(mod_file, mod_file)
)
self.assertEqual(result, expected_html)

@unittest.skipIf(sys.flags.optimize >= 2,
Expand All @@ -443,7 +474,9 @@ def test_text_doc(self):
expected_text = expected_text_pattern % (
(doc_loc,) +
expected_text_data_docstrings +
(inspect.getabsfile(pydoc_mod),))
(inspect.getabsfile(pydoc_mod),) +
expected_text_data_docstrings +
3*(inspect.getabsfile(pydoc_mod),))
self.assertEqual(expected_text, result)

def test_text_enum_member_with_value_zero(self):
Expand Down Expand Up @@ -658,7 +691,6 @@ def test_help_output_redirect(self):
old_pattern = expected_text_pattern
getpager_old = pydoc.getpager
getpager_new = lambda: (lambda x: x)
self.maxDiff = None

buf = StringIO()
helper = pydoc.Helper(output=buf)
Expand All @@ -680,7 +712,9 @@ def test_help_output_redirect(self):
expected_text = expected_help_pattern % (
(doc_loc,) +
expected_text_data_docstrings +
(inspect.getabsfile(pydoc_mod),))
(inspect.getabsfile(pydoc_mod),) +
expected_text_data_docstrings +
3*(inspect.getabsfile(pydoc_mod),))
self.assertEqual('', output.getvalue())
self.assertEqual('', err.getvalue())
self.assertEqual(expected_text, result)
Expand Down Expand Up @@ -814,6 +848,11 @@ class B(A)
| Configure resources of an item TAGORID.
|\x20\x20
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|\x20\x20
| __filename__ = '%s'
|\x20\x20
| ----------------------------------------------------------------------
| Methods inherited from A:
|\x20\x20
| a_size(self)
Expand All @@ -832,7 +871,7 @@ class B(A)
|\x20\x20
| __weakref__
| list of weak references to the object (if defined)
''' % __name__)
''' % (__name__, __file__))

doc = pydoc.render_doc(B, renderer=pydoc.HTMLDoc())
self.assertEqual(doc, '''\
Expand All @@ -858,6 +897,10 @@ class B(A)

<dl><dt><a name="B-itemconfigure"><strong>itemconfigure</strong></a>(self, tagOrId, cnf=None, **kw)</dt><dd><tt>Configure&nbsp;resources&nbsp;of&nbsp;an&nbsp;item&nbsp;TAGORID.</tt></dd></dl>

<hr>
Data and other attributes defined here:<br>
<dl><dt><strong>__filename__</strong> = '%s'</dl>

<hr>
Methods inherited from A:<br>
<dl><dt><a name="B-a_size"><strong>a_size</strong></a>(self)</dt><dd><tt>Return&nbsp;size</tt></dd></dl>
Expand All @@ -875,7 +918,7 @@ class B(A)
<dd><tt>list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object&nbsp;(if&nbsp;defined)</tt></dd>
</dl>
</td></tr></table>\
''' % __name__)
''' % (__name__, __file__))


class PydocImportTest(PydocBaseTest):
Expand Down Expand Up @@ -1380,6 +1423,8 @@ def test_keywords(self):
sorted(keyword.kwlist))

class PydocWithMetaClasses(unittest.TestCase):
maxDiff = None

@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
Expand All @@ -1400,7 +1445,7 @@ def ham(self):
helper = pydoc.Helper(output=output)
helper(DA)
expected_text = expected_dynamicattribute_pattern % (
(__name__,) + expected_text_data_docstrings[:2])
(__name__,) + expected_text_data_docstrings + (__file__,))
result = output.getvalue().strip()
self.assertEqual(expected_text, result)

Expand Down
6 changes: 3 additions & 3 deletions Lib/typing.py
Expand Up @@ -942,8 +942,8 @@ class _TypingEllipsis:
_TYPING_INTERNALS = ['__parameters__', '__orig_bases__', '__orig_class__',
'_is_protocol', '_is_runtime_protocol']

_SPECIAL_NAMES = ['__abstractmethods__', '__annotations__', '__dict__', '__doc__',
'__init__', '__module__', '__new__', '__slots__',
_SPECIAL_NAMES = ['__abstractmethods__', '__annotations__', '__filename__', '__dict__',
'__doc__', '__init__', '__module__', '__new__', '__slots__',
'__subclasshook__', '__weakref__']

# These special attributes will be not collected as protocol members.
Expand Down Expand Up @@ -1587,7 +1587,7 @@ def _make_nmtuple(name, types):
'_fields', '_field_defaults', '_field_types',
'_make', '_replace', '_asdict', '_source')

_special = ('__module__', '__name__', '__qualname__', '__annotations__')
_special = ('__filename__', '__module__', '__name__', '__qualname__', '__annotations__')


class NamedTupleMeta(type):
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Expand Up @@ -1715,6 +1715,7 @@ Mike Verdone
Jaap Vermeulen
Nikita Vetoshkin
Al Vezza
Thomas Viehmann
Petr Viktorin
Jacques A. Vidrine
John Viega
Expand Down
@@ -0,0 +1 @@
Add __filename__ attribute to classes for enhanced introspection.
15 changes: 15 additions & 0 deletions Python/bltinmodule.c
Expand Up @@ -9,6 +9,7 @@

_Py_IDENTIFIER(__builtins__);
_Py_IDENTIFIER(__dict__);
_Py_IDENTIFIER(__filename__);
_Py_IDENTIFIER(__prepare__);
_Py_IDENTIFIER(__round__);
_Py_IDENTIFIER(__mro_entries__);
Expand Down Expand Up @@ -104,6 +105,7 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *orig_bases;
PyObject *cls = NULL, *cell = NULL;
int isclass = 0; /* initialize to prevent gcc warning */
int err = 0;

if (nargs < 2) {
PyErr_SetString(PyExc_TypeError,
Expand Down Expand Up @@ -222,6 +224,19 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
NULL, 0, NULL, 0, NULL, 0, NULL,
PyFunction_GET_CLOSURE(func));
if (cell != NULL) {
if (PyDict_CheckExact(ns)) {
err = _PyDict_SetItemId(ns, &PyId___filename__, ((PyCodeObject*) PyFunction_GET_CODE(func))->co_filename);
}
else {
PyObject *filename_str = _PyUnicode_FromId(&PyId___filename__);
if (filename_str == NULL) {
goto error;
}
err = PyObject_SetItem(ns, filename_str, ((PyCodeObject*) PyFunction_GET_CODE(func))->co_filename);
}
if (err != 0) {
goto error;
}
if (bases != orig_bases) {
if (PyMapping_SetItemString(ns, "__orig_bases__", orig_bases) < 0) {
goto error;
Expand Down