diff --git a/CHANGES.txt b/CHANGES.txt index b7e116c1bf..4c47ca67cc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -51,6 +51,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER need actual installed system programs (add -live suffix). - Test runner reworked to use Python logging; the portion of the test suite which tests the runner was adjusted to match. + - Switch remaining "original style" docstring parameter listings to + Google style. RELEASE 4.10.1 - Sun, 16 Nov 2025 10:51:57 -0700 diff --git a/RELEASE.txt b/RELEASE.txt index 3a6d20e32f..04efae3a2f 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -53,6 +53,8 @@ IMPROVEMENTS - Used Gemini to refactor runtest.py to better organized the code and add docstrings. +- Switch remaining "original style" docstring parameter listings to Google style. + PACKAGING --------- diff --git a/SCons/Action.py b/SCons/Action.py index 505c6bbfc0..73a1be604a 100644 --- a/SCons/Action.py +++ b/SCons/Action.py @@ -332,11 +332,11 @@ def _object_instance_content(obj): """ Returns consistant content for a action class or an instance thereof - :Parameters: - - `obj` Should be either and action class or an instance thereof + Args: + obj: Should be either an action class or an instance thereof - :Returns: - bytearray or bytes representing the obj suitable for generating a signature from. + Returns: + bytearray or bytes representing the obj suitable for generating a signature from. """ retval = bytearray() diff --git a/SCons/Conftest.py b/SCons/Conftest.py index 5041f24d54..8a7eb597ce 100644 --- a/SCons/Conftest.py +++ b/SCons/Conftest.py @@ -765,11 +765,11 @@ def _YesNoResult(context, ret, key, text, comment = None) -> None: r""" Handle the result of a test with a "yes" or "no" result. - :Parameters: - - `ret` is the return value: empty if OK, error message when not. - - `key` is the name of the symbol to be defined (HAVE_foo). - - `text` is the source code of the program used for testing. - - `comment` is the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added. + Args: + ret: the return value, empty if OK, error message when not. + key: the name of the symbol to be defined (HAVE_foo). + text: the source code of the program used for testing. + comment: the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added. """ if key: _Have(context, key, not ret, comment) @@ -784,10 +784,10 @@ def _Have(context, key, have, comment = None) -> None: r""" Store result of a test in context.havedict and context.headerfilename. - :Parameters: - - `key` - is a "HAVE_abc" name. It is turned into all CAPITALS and non-alphanumerics are replaced by an underscore. - - `have` - value as it should appear in the header file, include quotes when desired and escape special characters! - - `comment` is the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added. + Args: + key: a "HAVE_abc" name. It is turned into all CAPITALS and non-alphanumerics are replaced by an underscore. + have: value as it should appear in the header file, include quotes when desired and escape special characters! + comment: is the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added. The value of "have" can be: @@ -795,8 +795,6 @@ def _Have(context, key, have, comment = None) -> None: - 0 - Feature is not defined, add "/\* #undef key \*/". Adding "undef" is what autoconf does. Not useful for the compiler, but it shows that the test was done. - number - Feature is defined to this number "#define key have". Doesn't work for 0 or 1, use a string then. - string - Feature is defined to this string "#define key have". - - """ key_up = key.upper() key_up = re.sub('[^A-Z0-9_]', '_', key_up) @@ -849,9 +847,9 @@ def _lang2suffix(lang): For an unrecognized language returns (None, None, msg). Where: - - lang = the unified language name - - suffix = the suffix, including the leading dot - - msg = an error message + lang: the unified language name + suffix: the suffix, including the leading dot + msg: an error message """ if not lang or lang in ["C", "c"]: return ("C", ".c", None) diff --git a/SCons/Defaults.py b/SCons/Defaults.py index c8f73eb160..502f43a0eb 100644 --- a/SCons/Defaults.py +++ b/SCons/Defaults.py @@ -45,6 +45,7 @@ import SCons.CacheDir import SCons.Environment import SCons.Errors +import SCons.Node.FS import SCons.PathList import SCons.Scanner.Dir import SCons.Subst @@ -606,7 +607,7 @@ def processDefines(defs) -> list[str]: # TODO: do we need to quote value if it contains space? dlist.append(f"{name}={value[0]}") else: - dlist.append(str(define[0])) + dlist.append(str(defs[0])) elif is_Dict(defs): for macro, value in defs.items(): if value is None: @@ -685,10 +686,13 @@ def __call__(self, *args, **kw): def __libversionflags(env, version_var, flags_var): """ if version_var is not empty, returns env[flags_var], otherwise returns None - :param env: - :param version_var: - :param flags_var: - :return: + + Args: + env: + version_var: + flags_var: + + Returns: """ try: if env.subst('$' + version_var): @@ -701,11 +705,14 @@ def __libversionflags(env, version_var, flags_var): def __lib_either_version_flag(env, version_var1, version_var2, flags_var): """ if $version_var1 or $version_var2 is not empty, returns env[flags_var], otherwise returns None - :param env: - :param version_var1: - :param version_var2: - :param flags_var: - :return: + + Args: + env: + version_var1: + version_var2: + flags_var: + + Returns: """ try: if env.subst('$' + version_var1) or env.subst('$' + version_var2): diff --git a/SCons/Node/FS.py b/SCons/Node/FS.py index 8e612a06cf..d2bbb1a911 100644 --- a/SCons/Node/FS.py +++ b/SCons/Node/FS.py @@ -48,10 +48,9 @@ import SCons.Errors import SCons.Memoize import SCons.Node -import SCons.Node.Alias +import SCons.SConsign import SCons.Subst import SCons.Util -import SCons.Warnings from SCons.Debug import logInstanceCreation, Trace from SCons.Node import BuildInfoBase, Node, NodeInfoBase from SCons.Util import hash_signature, hash_file_signature, hash_collect @@ -74,7 +73,6 @@ def sconsign_dir(node): """Return the .sconsign file info for this directory, creating it first if necessary.""" if not node._sconsign: - import SCons.SConsign node._sconsign = SCons.SConsign.ForDirectory(node) return node._sconsign @@ -382,8 +380,8 @@ def MkdirFunc(target: list[Base], source: list[Base], env: Environment) -> int: def get_MkdirBuilder() -> BuilderBase: global MkdirBuilder if MkdirBuilder is None: - import SCons.Builder - import SCons.Defaults + import SCons.Builder # pylint: disable-msg=import-outside-toplevel + import SCons.Defaults # pylint: disable-msg=import-outside-toplevel # "env" will get filled in by Executor.get_build_env() # calling SCons.Defaults.DefaultEnvironment() when necessary. MkdirBuilder = SCons.Builder.Builder(action = Mkdir, @@ -432,8 +430,9 @@ def __call__(self, *args, **kw) -> None: def enable(self, disk_check_type_list: list[str]) -> None: """ If the current object's disk_check_type matches any in the list passed - :param disk_check_type_list: List of disk checks to enable - :return: + + Args: + disk_check_type_list: List of disk checks to enable """ if self.disk_check_type in disk_check_type_list: self.func = self.do_check_function @@ -1035,7 +1034,14 @@ def diskcheck_match(self) -> None: pass def disambiguate(self, must_exist: bool = False) -> FileNode | DirNode: - """ + """Disamgbiguate (and morph) the node and return it. + + Args: + must_exist: if true, it is an error for there to be no + matching file on disk. + + Raises: + :exc:`UserError`: nothing on disk and *must_exist* is true. """ if self.isfile(): self.__class__ = File # type: ignore[assignment] @@ -1055,9 +1061,11 @@ def disambiguate(self, must_exist: bool = False) -> FileNode | DirNode: # with that name, in which case we can go ahead and call # self.srcnode() to create the right type of entry. srcdir = self.dir.srcnode() - if srcdir != self.dir and \ - srcdir.entry_exists_on_disk(self.name) and \ - self.srcnode().isdir(): + if ( + srcdir != self.dir + and srcdir.entry_exists_on_disk(self.name) + and self.srcnode().isdir() + ): self.__class__ = Dir # type: ignore[assignment] self._morph() elif must_exist: @@ -1080,7 +1088,7 @@ def rfile(self) -> FileNode: def scanner_key(self) -> str: return self.get_suffix() - def get_contents(self): + def get_contents(self) -> bytes: """Fetch the contents of the entry. Returns the exact binary contents of the file.""" return SCons.Node._get_contents_map[self._func_get_contents](self) @@ -1492,7 +1500,7 @@ def Dir(self, name: str, directory: DirNode | None = None, create: bool = True) """ return self._lookup(name, directory, Dir, create) - def VariantDir(self, variant_dir: str | DirNode, src_dir: str | DirNode, duplicate: int=1) -> None: + def VariantDir(self, variant_dir: str | DirNode, src_dir: str | DirNode, duplicate: bool = True) -> None: """Link the supplied variant directory to the source directory for purposes of building files.""" @@ -1846,12 +1854,13 @@ def rel_path(self, other: Base) -> str: return result + # TODO: mutable default def get_env_scanner(self, env: Environment, kw: dict[str, Any] | None = {}) -> ScannerBase: - import SCons.Defaults + import SCons.Defaults # pylint: disable=import-outside-toplevel return SCons.Defaults.DirEntryScanner def get_target_scanner(self) -> ScannerBase: - import SCons.Defaults + import SCons.Defaults # pylint: disable=import-outside-toplevel return SCons.Defaults.DirEntryScanner def get_found_includes(self, env: Environment, scanner: ScannerBase | None, path: str) -> list[Node]: @@ -1975,9 +1984,8 @@ def rdir(self) -> DirNode: for dir in self.dir.get_all_rdirs(): try: node = dir.entries[norm_name] except KeyError: node = dir.dir_on_disk(self.name) - if node and node.exists() and \ - (isinstance(dir, Dir) or isinstance(dir, Entry)): - return node + if node and node.exists() and isinstance(dir, (Dir, Entry)): + return node return self def sconsign(self) -> SConsignDatabase: @@ -2141,9 +2149,8 @@ def srcdir_find_file(self, filename: str) -> tuple[FileNode, DirNode] | tuple[No pass def func(node: Node) -> FileNode | None: - if (isinstance(node, File) or isinstance(node, Entry)) and \ - (node.is_derived() or node.exists()): - return node + if isinstance(node, (File, Entry)) and (node.is_derived() or node.exists()): + return node return None norm_name = _my_normcase(filename) @@ -2407,7 +2414,7 @@ def __init__(self, drive: str, fs: FS) -> None: self._morph() - self.duplicate = 0 + self.duplicate = False self._lookupDict = {'': self, '/': self} self.root = self @@ -2604,7 +2611,7 @@ def convert_to_sconsign(self) -> None: if os_sep_is_slash: node_to_str = str else: - def node_to_str(n): + def node_to_str(n) -> str: try: s = n.get_internal_path() except AttributeError: @@ -2612,6 +2619,7 @@ def node_to_str(n): else: s = s.replace(OS_SEP, '/') return s + for attr in ['bsources', 'bdepends', 'bimplicit']: try: val = getattr(self, attr) @@ -2893,7 +2901,6 @@ def convert_old_entry(self, old_entry: SConsignEntry) -> SConsignEntry: # If it was actually a build signature, then it will cause a # rebuild anyway when it doesn't match the new content signature, # but that's probably the best we can do. - import SCons.SConsign new_entry = SCons.SConsign.SConsignEntry() new_entry.binfo = self.new_binfo() binfo = new_entry.binfo @@ -2931,7 +2938,6 @@ def get_stored_info(self) -> SConsignEntry: try: sconsign_entry = self.dir.sconsign().get_entry(self.name) except (KeyError, OSError): - import SCons.SConsign sconsign_entry = SCons.SConsign.SConsignEntry() sconsign_entry.binfo = self.new_binfo() sconsign_entry.ninfo = self.new_ninfo() @@ -2994,8 +3000,7 @@ def _createDir(self) -> None: self.dir._create() def push_to_cache(self) -> bool: - """Try to push the node into a cache - """ + """Try to push the node into a cache.""" # This should get called before the Nodes' .built() method is # called, which would clear the build signature if the file has # a source scanner. @@ -3085,15 +3090,15 @@ def release_target_info(self) -> None: self._memo.pop('rfile', None) self.prerequisites = None # Cleanup lists, but only if they're empty - if not len(self.ignore_set): + if not self.ignore_set: self.ignore_set = None - if not len(self.implicit_set): + if not self.implicit_set: self.implicit_set = None - if not len(self.depends_set): + if not self.depends_set: self.depends_set = None - if not len(self.ignore): + if not self.ignore: self.ignore = None - if not len(self.depends): + if not self.depends: self.depends = None # Mark this node as done, we only have to release # the memory once... @@ -3361,8 +3366,12 @@ def _build_dependency_map(self, binfo: BuildInfoBase) -> dict[FileNode, BuildInf len(binfo.bimplicitsigs)) == 0: return {} - binfo.dependency_map = { child:signature for child, signature in zip(chain(binfo.bsources, binfo.bdepends, binfo.bimplicit), - chain(binfo.bsourcesigs, binfo.bdependsigs, binfo.bimplicitsigs))} + binfo.dependency_map = dict( + zip( + chain(binfo.bsources, binfo.bdepends, binfo.bimplicit), + chain(binfo.bsourcesigs, binfo.bdependsigs, binfo.bimplicitsigs), + ) + ) return binfo.dependency_map @@ -3377,8 +3386,8 @@ def _add_strings_to_dependency_map(self, dmap: dict[str | FileNode, BuildInfoBas # print("DMAP:%s"%id(dmap)) if first_string not in dmap: - string_dict = {str(child): signature for child, signature in dmap.items()} - dmap.update(string_dict) + string_dict = {str(child): signature for child, signature in dmap.items()} + dmap.update(string_dict) return dmap def _get_previous_signatures(self, dmap: dict[str | FileNode, BuildInfoBase]) -> BuildInfoBase | None: @@ -3531,9 +3540,11 @@ def changed_timestamp_newer(self, target: FileNode, prev_ni: NodeInfoBase, repo_ def changed_timestamp_match(self, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: """ Return True if the timestamps don't match or if there is no previous timestamp - :param target: - :param prev_ni: Information about the node from the previous build - :return: + + Args: + target: + prev_ni: Information about the node from the previous build + repo_node: """ try: return self.get_timestamp() != prev_ni.timestamp @@ -3590,8 +3601,7 @@ def rfile(self) -> FileNode: node = repo_dir.file_on_disk(self.name) if node and node.exists() and \ - (isinstance(node, File) or isinstance(node, Entry) - or not node.is_derived()): + (isinstance(node, (File, Entry)) or not node.is_derived()): result = node # Copy over our local attributes to the repository # Node so we identify shared object files in the @@ -3611,9 +3621,10 @@ def rfile(self) -> FileNode: return result def find_repo_file(self) -> list[FileNode]: - """ - For this node, find if there exists a corresponding file in one or more repositories - :return: list of corresponding files in repositories + """For this node, find if there exists a corresponding file in one or more repositories + + Returns: + list of corresponding files in repositories """ retvals = [] @@ -3625,9 +3636,8 @@ def find_repo_file(self) -> list[FileNode]: node = repo_dir.file_on_disk(self.name) if node and node.exists() and \ - (isinstance(node, File) or isinstance(node, Entry) - or not node.is_derived()): - retvals.append(node) + (isinstance(node, (File, Entry)) or not node.is_derived()): + retvals.append(node) return retvals @@ -3824,7 +3834,7 @@ def find_file(self, filename: str, paths: list[DirNode], verbose: bool | str | C find_file = FileFinder().find_file -def invalidate_node_memos(targets: list[Base | str]) -> None: +def invalidate_node_memos(targets: str | list[Base | str]) -> None: """ Invalidate the memoized values of all Nodes (files or directories) that are associated with the given entries. Has been added to diff --git a/SCons/Tool/applelink.py b/SCons/Tool/applelink.py index 41c968f1fc..6e8142d154 100644 --- a/SCons/Tool/applelink.py +++ b/SCons/Tool/applelink.py @@ -49,14 +49,18 @@ class AppleLinkInvalidCompatibilityVersionException(Exception): def _applelib_check_valid_version(version_string): - """ - Check that the version # is valid. + """Check that the version # is valid. + X[.Y[.Z]] where X 0-65535 where Y either not specified or 0-255 where Z either not specified or 0-255 - :param version_string: - :return: + + Args: + version_string: + + Returns: + A tuple of the outcome and an information string (empty if True) """ parts = version_string.split('.') if len(parts) > 3: @@ -83,11 +87,14 @@ def _applelib_currentVersionFromSoVersion(source, target, env, for_signature) -> Otherwise if APPLELINK_CURRENT_VERSION is not specified, env['SHLIBVERSION'] will be used. - :param source: - :param target: - :param env: - :param for_signature: - :return: A string providing the flag to specify the current_version of the shared library + Args: + source: + target: + env: + for_signature: + + Returns: + A string providing the flag to specify the current_version of the shared library """ if env.get('APPLELINK_NO_CURRENT_VERSION', False): return "" @@ -114,11 +121,14 @@ def _applelib_compatVersionFromSoVersion(source, target, env, for_signature) -> Otherwise if APPLELINK_COMPATIBILITY_VERSION is not specified the first two parts of env['SHLIBVERSION'] will be used with a .0 appended. - :param source: - :param target: - :param env: - :param for_signature: - :return: A string providing the flag to specify the compatibility_version of the shared library + Args: + source: + target: + env: + for_signature: + + Returns: + A string providing the flag to specify the compatibility_version of the shared library """ if env.get('APPLELINK_NO_COMPATIBILITY_VERSION', False): return "" diff --git a/SCons/Tool/cyglink.py b/SCons/Tool/cyglink.py index ece67e906e..6f0960084e 100644 --- a/SCons/Tool/cyglink.py +++ b/SCons/Tool/cyglink.py @@ -78,12 +78,16 @@ def cyglink_shlib_symlink_emitter(target, source, env, **kw): """ On cygwin, we only create a symlink from the non-versioned implib to the versioned implib. We don't version the shared library itself. - :param target: - :param source: - :param env: - :param kw: - :return: - """ + + Args: + target: + source: + env: + kw: + + Returns: + A tuple of target, source + """ verbose = True if 'variable_prefix' in kw: diff --git a/SCons/Tool/linkCommon/LoadableModule.py b/SCons/Tool/linkCommon/LoadableModule.py index 77a06edddf..0e44b7027f 100644 --- a/SCons/Tool/linkCommon/LoadableModule.py +++ b/SCons/Tool/linkCommon/LoadableModule.py @@ -30,14 +30,20 @@ def ldmod_symlink_emitter(target, source, env, **kw): return shlib_symlink_emitter(target, source, env, variable_prefix='LDMODULE') -def _get_ldmodule_stem(target, source, env, for_signature): - """ - Get the basename for a library (so for libxyz.so, return xyz) - :param target: - :param source: - :param env: - :param for_signature: - :return: +def _get_ldmodule_stem(target, source, env, for_signature) -> str: + """Get the basename for a library. + + Strips off the loadable module prefix and suffix defined in the + construction environment (e.g. ``libxyz.so`` -> ``xyz``) + + Args: + target: + source: + env: + for_signature: + + Returns: + The extracted basename """ target_name = str(target) ldmodule_prefix = env.subst('$LDMODULEPREFIX') @@ -83,17 +89,15 @@ def _LDMODULEVERSION(target, source, env, for_signature): return "" def setup_loadable_module_logic(env) -> None: - """ - Just the logic for loadable modules + """Just the logic for loadable modules For most platforms, a loadable module is the same as a shared library. Platforms which are different can override these, but setting them the same means that LoadableModule works everywhere. - :param env: - :return: + Args: + env: """ - createLoadableModuleBuilder(env) env['_get_ldmodule_stem'] = _get_ldmodule_stem @@ -128,4 +132,4 @@ def setup_loadable_module_logic(env) -> None: '$_LIBDIRFLAGS $_LIBFLAGS ' env['LDMODULEVERSION'] = '$SHLIBVERSION' - env['LDMODULENOVERSIONSYMLINKS'] = '$SHLIBNOVERSIONSYMLINKS' \ No newline at end of file + env['LDMODULENOVERSIONSYMLINKS'] = '$SHLIBNOVERSIONSYMLINKS' diff --git a/testing/framework/TestCmd.py b/testing/framework/TestCmd.py index 5c7de8049f..b4edc1b624 100644 --- a/testing/framework/TestCmd.py +++ b/testing/framework/TestCmd.py @@ -534,7 +534,7 @@ def pass_test(self=None, condition: bool = True, function=None) -> None: a condition argument is supplied; if so the completion processing takes place only if the condition is true. - the test passes only if the condition is true. + The test passes only if the condition is true. Args: self: a test class instance. Must be passed in explicitly @@ -551,16 +551,20 @@ def pass_test(self=None, condition: bool = True, function=None) -> None: sys.exit(0) -def match_exact(lines=None, matches=None, newline=os.sep): +def match_exact( + lines: str | list[str] | None = None, + matches: str | list[str] | None = None, + newline: str = os.sep, +) -> int | None: """Match function using exact match. - :param lines: data lines - :type lines: str or list[str] - :param matches: expected lines to match - :type matches: str or list[str] - :param newline: line separator - :returns: None on failure, 1 on success. + Args: + lines: Data lines. + matches: Expected lines to match. + newline: Line separator. + Returns: + None on failure, 1 on success. """ if isinstance(lines, bytes): newline = to_bytes(newline) @@ -577,19 +581,22 @@ def match_exact(lines=None, matches=None, newline=os.sep): return 1 -def match_caseinsensitive(lines=None, matches=None): +def match_caseinsensitive( + lines: str | list[str] | None = None, + matches: str | list[str] | None = None, +) -> int | None: """Match function using case-insensitive matching. Only a simplistic comparison is done, based on casefolding the strings. This may still fail but is the suggestion of the Unicode Standard. - :param lines: data lines - :type lines: str or list[str] - :param matches: expected lines to match - :type matches: str or list[str] - :returns: None on failure, 1 on success. + Args: + lines: Data lines. + matches: Expected lines to match. + Returns: + None on failure, 1 on success. """ if not is_List(lines): lines = lines.split("\n") @@ -603,15 +610,18 @@ def match_caseinsensitive(lines=None, matches=None): return 1 -def match_re(lines=None, res=None): +def match_re( + lines: str | list[str] | None = None, + res: str | list[str] | None = None, +) -> int | None: """Match function using line-by-line regular expression match. - :param lines: data lines - :type lines: str or list[str] - :param res: regular expression(s) for matching - :type res: str or list[str] - :returns: None on failure, 1 on success. + Args: + lines: Data lines. + res: Regular expression(s) for matching. + Returns: + None on failure, 1 on success. """ if not is_List(lines): # CRs mess up matching (Windows) so split carefully @@ -635,18 +645,21 @@ def match_re(lines=None, res=None): return 1 -def match_re_dotall(lines=None, res=None): +def match_re_dotall( + lines: str | list[str] | None = None, + res: str | list[str] | None = None, +) -> Match[str] | None: """Match function using regular expression match. - Unlike match_re, the arguments are converted to strings (if necessary) + Unlike :math:`match_re`, the arguments are converted to strings (if necessary) and must match exactly. - :param lines: data lines - :type lines: str or list[str] - :param res: regular expression(s) for matching - :type res: str or list[str] - :returns: a match object on match, else None, like re.match + Args: + lines: Data lines. + res: Regular expression(s) for matching. + Returns: + A match object on match, else None, like :meth:`re.match`. """ if not isinstance(lines, str): lines = "\n".join(lines) @@ -735,7 +748,7 @@ def diff_re( are regular expressions. This is a really dumb thing that just compares each line in turn, so it doesn't look for chunks of matching lines and the like--but at least it lets - you know exactly which line first didn't compare correctl... + you know exactly which line first didn't compare correctly. Raises: re.error: if a regex fails to compile @@ -2152,7 +2165,7 @@ def do_chmod(fname) -> None: do_chmod(os.path.join(dirpath, name)) do_chmod(top) - def write(self, file, content, mode: str = 'wb'): + def write(self, file: str | list[str], content: str | bytes, mode: str = 'wb'): """Writes data to file. The file is created under the temporary working directory. @@ -2160,13 +2173,11 @@ def write(self, file, content, mode: str = 'wb'): write is converted to the required type rather than failing if there is a str/bytes mistmatch. - :param file: name of file to write to. If a list, treated - as components of a path and concatenated into a path. - :type file: str or list(str) - :param content: data to write. - :type content: str or bytes - :param mode: file mode, default is binary. - :type mode: str + Args: + file: Name of file to write to. If a list, treated as + components of a path and concatenated into a path. + content: Data to write. + mode: File mode, default is binary. """ file = self.canonicalize(file) if mode[0] != 'w':