From 2745c575bd8b066ab5142c0959a528a026b0adc4 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:31:57 +0100 Subject: [PATCH] Avoid mutable default arguments While they are not always an actual bug, they are certainly a Python anti-pattern and possible the source of future bugs. In at least one case, the default mutable argument `config` is actually modified later on by the function. --- numpydoc/docscrape.py | 18 +++++++++++++----- numpydoc/docscrape_sphinx.py | 20 +++++++++++++++----- numpydoc/tests/test_docscrape.py | 2 +- numpydoc/xref.py | 2 +- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 383d1924..9e306aea 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -136,7 +136,7 @@ class NumpyDocString(Mapping): 'index': {} } - def __init__(self, docstring, config={}): + def __init__(self, docstring, config=None): orig_docstring = docstring docstring = textwrap.dedent(docstring).split('\n') @@ -553,7 +553,7 @@ def dedent_lines(lines): class FunctionDoc(NumpyDocString): - def __init__(self, func, role='func', doc=None, config={}): + def __init__(self, func, role='func', doc=None, config=None): self._f = func self._role = role # e.g. "func" or "meth" @@ -561,6 +561,8 @@ def __init__(self, func, role='func', doc=None, config={}): if func is None: raise ValueError("No function or docstring given") doc = inspect.getdoc(func) or '' + if config is None: + config = {} NumpyDocString.__init__(self, doc, config) def get_func(self): @@ -590,8 +592,10 @@ def __str__(self): class ObjDoc(NumpyDocString): - def __init__(self, obj, doc=None, config={}): + def __init__(self, obj, doc=None, config=None): self._f = obj + if config is None: + config = {} NumpyDocString.__init__(self, doc, config=config) @@ -600,7 +604,7 @@ class ClassDoc(NumpyDocString): extra_public_methods = ['__call__'] def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc, - config={}): + config=None): if not inspect.isclass(cls) and cls is not None: raise ValueError("Expected a class or None, but got %r" % cls) self._cls = cls @@ -610,6 +614,8 @@ def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc, else: ALL = object() + if config is None: + config = {} self.show_inherited_members = config.get( 'show_inherited_class_members', True) @@ -679,7 +685,7 @@ def _is_show_member(self, name): return True -def get_doc_object(obj, what=None, doc=None, config={}): +def get_doc_object(obj, what=None, doc=None, config=None): if what is None: if inspect.isclass(obj): what = 'class' @@ -689,6 +695,8 @@ def get_doc_object(obj, what=None, doc=None, config={}): what = 'function' else: what = 'object' + if config is None: + config = {} if what == 'class': return ClassDoc(obj, func_doc=FunctionDoc, doc=doc, config=config) diff --git a/numpydoc/docscrape_sphinx.py b/numpydoc/docscrape_sphinx.py index 55823c15..91042155 100644 --- a/numpydoc/docscrape_sphinx.py +++ b/numpydoc/docscrape_sphinx.py @@ -18,7 +18,9 @@ class SphinxDocString(NumpyDocString): - def __init__(self, docstring, config={}): + def __init__(self, docstring, config=None): + if config is None: + config = {} NumpyDocString.__init__(self, docstring, config=config) self.load_config(config) @@ -392,25 +394,31 @@ def __str__(self, indent=0, func_role="obj"): class SphinxFunctionDoc(SphinxDocString, FunctionDoc): - def __init__(self, obj, doc=None, config={}): + def __init__(self, obj, doc=None, config=None): + if config is None: + config = {} self.load_config(config) FunctionDoc.__init__(self, obj, doc=doc, config=config) class SphinxClassDoc(SphinxDocString, ClassDoc): - def __init__(self, obj, doc=None, func_doc=None, config={}): + def __init__(self, obj, doc=None, func_doc=None, config=None): + if config is None: + config = {} self.load_config(config) ClassDoc.__init__(self, obj, doc=doc, func_doc=None, config=config) class SphinxObjDoc(SphinxDocString, ObjDoc): - def __init__(self, obj, doc=None, config={}): + def __init__(self, obj, doc=None, config=None): + if config is None: + config = {} self.load_config(config) ObjDoc.__init__(self, obj, doc=doc, config=config) # TODO: refactor to use docscrape.get_doc_object -def get_doc_object(obj, what=None, doc=None, config={}, builder=None): +def get_doc_object(obj, what=None, doc=None, config=None, builder=None): if what is None: if inspect.isclass(obj): what = 'class' @@ -421,6 +429,8 @@ def get_doc_object(obj, what=None, doc=None, config={}, builder=None): else: what = 'object' + if config is None: + config = {} template_dirs = [os.path.join(os.path.dirname(__file__), 'templates')] if builder is not None: template_loader = BuiltinTemplateLoader() diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 0706b6d0..00ed9f7e 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -225,7 +225,7 @@ def test_returns(doc): assert arg_type == 'ndarray' assert desc[0].startswith('The drawn samples') assert desc[-1].endswith('distribution.') - + arg, arg_type, desc = doc['Returns'][1] assert arg == '' assert arg_type == 'list of str' diff --git a/numpydoc/xref.py b/numpydoc/xref.py index 7e98b96e..ee3954bb 100644 --- a/numpydoc/xref.py +++ b/numpydoc/xref.py @@ -108,7 +108,7 @@ def make_xref(param_type, xref_aliases, xref_ignore): to fully qualified names that can be cross-referenced. xref_ignore : set or "all" A set containing words not to cross-reference. Instead of a set, the - string 'all' can be given to ignore all unrecognized terms. + string 'all' can be given to ignore all unrecognized terms. Unrecognized terms include those that are not in `xref_aliases` and are not already wrapped in a reST role.