From ed4a985fd8bea1a1eb0cbb90c81e1939ff07ece1 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Thu, 3 Nov 2016 21:49:39 +0100 Subject: [PATCH 01/10] Added changes for 2.9 release --- CHANGES.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..005d6721 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,34 @@ +# Release Notes + +## Unreleased + +#### New Features + * support for `os.scandir` (Python >= 3.5) + * option to not fake modules named `path` + * `glob.glob`, `glob.iglob`: support for `recursive` argument + * support for `glob.iglob` + +## Version 2.9 + +#### New Features + * `io.open`, `os.open`: support for `encoding` argument + * `os.makedirs`: support for `exist_ok` argument + * support for fake `io.open()` + * support for mount points + * support for hard links + * support for float times (mtime, ctime) + * Windows support: + * support for alternative path separator (Windows) + * support for case-insensitive filesystems + * supprt for drive letters and UNC paths + * support for filesystem size + * `shutil.rmtree`: support for `ignore_errors` and `onerror` arguments + * support for `os.fsync()` and `os.fdatasync()` + * `os.walk`: Support for `followlinks` argument + +#### Fixes + * `shutil` functions like `make_archive` do not work with pyfakefs (#104) + * file permissions on deletion not correctly handled (#27) + * `shutil.copy` error with bytes contents (#105) + * mtime and ctime not updated on content changes + * Reading from fake block devices doesn't work (#24) From 27edad043b0a014c033b7d5f20f76b6565f816ff Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Fri, 4 Nov 2016 20:09:18 +0100 Subject: [PATCH 02/10] Added some release notes for previous version - added default .pylintrc with changes for function and method names used in pyfakefs --- .pylintrc | 409 +++++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGES.md | 34 ++++- 2 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..2f947e92 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,409 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=dict-iter-method,buffer-builtin,cmp-method,apply-builtin,standarderror-builtin,backtick,long-builtin,unicode-builtin,import-star-module-level,parameter-unpacking,raising-string,raw_input-builtin,execfile-builtin,xrange-builtin,getslice-method,hex-method,old-octal-literal,coerce-builtin,indexing-exception,no-absolute-import,using-cmp-argument,old-ne-operator,map-builtin-not-iterating,long-suffix,dict-view-method,nonzero-method,delslice-method,old-division,coerce-method,oct-method,next-method-called,useless-suppression,suppressed-message,reduce-builtin,unichr-builtin,intern-builtin,zip-builtin-not-iterating,setslice-method,print-statement,input-builtin,filter-builtin-not-iterating,range-builtin-not-iterating,metaclass-assignment,round-builtin,old-raise-syntax,reload-builtin,basestring-builtin,file-builtin,cmp-builtin,unpacking-in-except + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct method names +# added camel case starting with capital letter +method-rgx=[a-z_][a-z0-9_]{2,30}|_{0,2}[A-Z][A-Za-z0-9]+$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct function names +# added camel case starting with capital letter +function-rgx=[a-z_][a-z0-9_]{2,30}|_{0,2}[A-Z][A-Za-z0-9]+$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line as per Google coding style. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/CHANGES.md b/CHANGES.md index 005d6721..5d6c816c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ -# Release Notes +# Release Notes +The release versions are PyPi releases. ## Unreleased @@ -32,3 +33,34 @@ * `shutil.copy` error with bytes contents (#105) * mtime and ctime not updated on content changes * Reading from fake block devices doesn't work (#24) + +## Version 2.7 + +### Infrastructure + * moved repository from GoogleCode to GitHub, merging 3 projects + * added continous integration testing with Travis CI + * added usage documentation in project wiki + * better support for pypi releases + +#### New Features + * added direct unit test support in `fake_filesystem_unittest` + (transparently patches all calls to faked implementations) + * added support for doctests + * added support for cygwin + * better support for Python 3 + +#### Fixes + * `chown` incorrectly accepts non-integer uid/gid arguments + * incorrect behavior of `relpath`, `abspath` and `normpath` on Windows. + * Python 3 `open` in binary mode not working (#32) + * `mkstemp` returns no valid file descriptor (#19) + * `open` methods lack `IOError` for prohibited operations (#18) + * incorrectly resolved relative path (#3) + * `FakeFileOpen` keyword args do not match the `__builtin__` equivalents (#5) + * relative paths not supported (#16, #17) + +## Older Versions +As there have been three different projects that have been merged together +for release 2.7, no older release notes are given. +The following versions are still avaliable in PyPi: + * 1.1, 1.2, 2.0, 2.1, 2.2, 2.3 and 2.4 From 965a441e18ea1a8ce12623dfb27626f2f373585b Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Fri, 4 Nov 2016 20:28:47 +0100 Subject: [PATCH 03/10] Added fake_pathlib implementation - added implementation and test for fake pathlib - moved some functionality from FakeOsModule to FakeFilesystem to be able to use it from fake pathlib - added follow_symlinks argument to to some funxtions in FakeOsModule (stat, chmod, chown, access, utime) - added replace() and lchmod() - fixes #29 --- all_tests.py | 10 +- fake_filesystem_test.py | 153 ++++++++ fake_filesystem_unittest_test.py | 13 + fake_pathlib_test.py | 457 ++++++++++++++++++++++ pyfakefs/fake_filesystem.py | 479 +++++++++++++++-------- pyfakefs/fake_filesystem_unittest.py | 22 +- pyfakefs/fake_pathlib.py | 548 +++++++++++++++++++++++++++ 7 files changed, 1524 insertions(+), 158 deletions(-) create mode 100644 fake_pathlib_test.py create mode 100644 pyfakefs/fake_pathlib.py diff --git a/all_tests.py b/all_tests.py index 59b13007..253f74a1 100755 --- a/all_tests.py +++ b/all_tests.py @@ -17,6 +17,7 @@ """A test suite that runs all tests for pyfakefs at once.""" import unittest +import sys import fake_filesystem_glob_test import fake_filesystem_shutil_test @@ -26,6 +27,9 @@ import fake_filesystem_unittest_test import example_test +if sys.version_info >= (3, 4): + import fake_pathlib_test + class AllTests(unittest.TestSuite): """A test suite that runs all tests for pyfakefs at once.""" @@ -41,11 +45,13 @@ def suite(self): # pylint: disable-msg=C6409 loader.loadTestsFromModule(fake_filesystem_unittest_test), loader.loadTestsFromModule(example_test), ]) + if sys.version_info >= (3, 4): + self.addTests([ + loader.loadTestsFromModule(fake_pathlib_test) + ]) return self if __name__ == '__main__': - import sys - result = unittest.TextTestRunner(verbosity=2).run(AllTests().suite()) sys.exit(int(not result.wasSuccessful())) diff --git a/fake_filesystem_test.py b/fake_filesystem_test.py index ffa3a58d..e222df9f 100755 --- a/fake_filesystem_test.py +++ b/fake_filesystem_test.py @@ -864,6 +864,21 @@ def testStat(self): self.assertTrue(stat.S_IFREG & self.os.stat(file_path).st_mode) self.assertEqual(5, self.os.stat(file_path)[stat.ST_SIZE]) + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testStatNoFollowSymlinks(self): + """Test that stat with follow_symlinks=False behaves like lstat.""" + directory = 'xyzzy' + base_name = 'plugh' + file_contents = 'frobozz' + # Just make sure we didn't accidentally make our test data meaningless. + self.assertNotEqual(len(base_name), len(file_contents)) + file_path = '%s/%s' % (directory, base_name) + link_path = '%s/link' % directory + self.filesystem.CreateFile(file_path, contents=file_contents) + self.filesystem.CreateLink(link_path, base_name) + self.assertEqual(len(file_contents), self.os.stat(file_path, follow_symlinks=False)[stat.ST_SIZE]) + self.assertEqual(len(base_name), self.os.stat(link_path, follow_symlinks=False)[stat.ST_SIZE]) + @unittest.skipIf(TestCase.is_windows and sys.version_info < (3, 3), 'Links are not supported under Windows before Python 3.3') def testLstat(self): @@ -1083,6 +1098,23 @@ def testRenameToExistentFile(self): self.assertEqual('test contents 1', self.filesystem.GetObject(new_file_path).contents) + @unittest.skipIf(sys.version_info < (3, 3), 'replace is new in Python 3.3') + def testReplaceToExistentFile(self): + """Replaces an existing file (does not work with `rename()` under Windows). + """ + directory = 'xyzzy' + old_file_path = '%s/plugh_old' % directory + new_file_path = '%s/plugh_new' % directory + self.filesystem.CreateFile(old_file_path, contents='test contents 1') + self.filesystem.CreateFile(new_file_path, contents='test contents 2') + self.assertTrue(self.filesystem.Exists(old_file_path)) + self.assertTrue(self.filesystem.Exists(new_file_path)) + self.os.replace(old_file_path, new_file_path) + self.assertFalse(self.filesystem.Exists(old_file_path)) + self.assertTrue(self.filesystem.Exists(new_file_path)) + self.assertEqual('test contents 1', + self.filesystem.GetObject(new_file_path).contents) + def testRenameToNonexistentDir(self): """Can rename a file to a name in a nonexistent dir.""" directory = 'xyzzy' @@ -1488,6 +1520,30 @@ def testAccess400(self): self.assertFalse(self.os.access(path, self.rwx)) self.assertFalse(self.os.access(path, self.rw)) + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testAccessSymlink(self): + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + self.os.chmod(link_path, 0o400) + + # test file + self.assertTrue(self.os.access(link_path, self.os.F_OK)) + self.assertTrue(self.os.access(link_path, self.os.R_OK)) + self.assertFalse(self.os.access(link_path, self.os.W_OK)) + self.assertFalse(self.os.access(link_path, self.os.X_OK)) + self.assertFalse(self.os.access(link_path, self.rwx)) + self.assertFalse(self.os.access(link_path, self.rw)) + + # test link itself + self.assertTrue(self.os.access(link_path, self.os.F_OK, follow_symlinks=False)) + self.assertTrue(self.os.access(link_path, self.os.R_OK, follow_symlinks=False)) + self.assertTrue(self.os.access(link_path, self.os.W_OK, follow_symlinks=False)) + self.assertTrue(self.os.access(link_path, self.os.X_OK, follow_symlinks=False)) + self.assertTrue(self.os.access(link_path, self.rwx, follow_symlinks=False)) + self.assertTrue(self.os.access(link_path, self.rw, follow_symlinks=False)) + def testAccessNonExistentFile(self): # set up path = '/non/existent/file' @@ -1511,6 +1567,46 @@ def testChmod(self): self.assertTrue(st.st_mode & stat.S_IFREG) self.assertFalse(st.st_mode & stat.S_IFDIR) + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testChmodFollowSymlink(self): + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + self.os.chmod(link_path, 0o6543) + + st = self.os.stat(link_path) + self.assertModeEqual(0o6543, st.st_mode) + st = self.os.stat(link_path, follow_symlinks=False) + self.assertModeEqual(0o777, st.st_mode) + + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testChmodNoFollowSymlink(self): + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + self.os.chmod(link_path, 0o6543, follow_symlinks=False) + + st = self.os.stat(link_path) + self.assertModeEqual(0o666, st.st_mode) + st = self.os.stat(link_path, follow_symlinks=False) + self.assertModeEqual(0o6543, st.st_mode) + + @unittest.skipIf(TestCase.is_windows, 'lchmod not supported in Windows') + def testLchmod(self): + """lchmod shall behave like chmod with follow_symlinks=True since Python 3.3""" + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + self.os.lchmod(link_path, 0o6543) + + st = self.os.stat(link_path) + self.assertModeEqual(0o666, st.st_mode) + st = self.os.lstat(link_path) + self.assertModeEqual(0o6543, st.st_mode) + def testChmodDir(self): # set up path = '/some_dir' @@ -1642,6 +1738,33 @@ def testUtimeDir(self): self.assertEqual(1.0, st.st_atime) self.assertEqual(2.0, st.st_mtime) + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testUtimeFollowSymlinks(self): + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + + self.os.utime(link_path, (1, 2)) + st = self.os.stat(link_path) + self.assertEqual(1, st.st_atime) + self.assertEqual(2, st.st_mtime) + + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testUtimeNoFollowSymlinks(self): + path = '/some_file' + self._CreateTestFile(path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, path) + + self.os.utime(link_path, (1, 2), follow_symlinks=False) + st = self.os.stat(link_path) + self.assertNotEqual(1, st.st_atime) + self.assertNotEqual(2, st.st_mtime) + st = self.os.stat(link_path, follow_symlinks=False) + self.assertEqual(1, st.st_atime) + self.assertEqual(2, st.st_mtime) + def testUtimeNonExistent(self): # set up path = '/non/existent/file' @@ -1693,6 +1816,36 @@ def testChownExistingFile(self): self.assertEqual(st[stat.ST_UID], 200) self.assertEqual(st[stat.ST_GID], 201) + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testChownFollowSymlink(self): + file_path = 'some_file' + self.filesystem.CreateFile(file_path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, file_path) + + self.os.chown(link_path, 100, 101) + st = self.os.stat(link_path) + self.assertEqual(st[stat.ST_UID], 100) + self.assertEqual(st[stat.ST_GID], 101) + st = self.os.stat(link_path, follow_symlinks=False) + self.assertNotEqual(st[stat.ST_UID], 100) + self.assertNotEqual(st[stat.ST_GID], 101) + + @unittest.skipIf(sys.version_info < (3, 3), 'follow_symlinks new in Python 3.3') + def testChownNoFollowSymlink(self): + file_path = 'some_file' + self.filesystem.CreateFile(file_path) + link_path = '/link_to_some_file' + self.filesystem.CreateLink(link_path, file_path) + + self.os.chown(link_path, 100, 101, follow_symlinks=False) + st = self.os.stat(link_path) + self.assertNotEqual(st[stat.ST_UID], 100) + self.assertNotEqual(st[stat.ST_GID], 101) + st = self.os.stat(link_path, follow_symlinks=False) + self.assertEqual(st[stat.ST_UID], 100) + self.assertEqual(st[stat.ST_GID], 101) + def testChownBadArguments(self): """os.chown() with bad args (Issue #30)""" file_path = 'some_file' diff --git a/fake_filesystem_unittest_test.py b/fake_filesystem_unittest_test.py index 748039de..83edea87 100755 --- a/fake_filesystem_unittest_test.py +++ b/fake_filesystem_unittest_test.py @@ -25,6 +25,9 @@ import tempfile import sys +if sys.version_info >= (3, 4): + import pathlib + if sys.version_info < (2, 7): import unittest2 as unittest else: @@ -127,6 +130,16 @@ def test_tempdirectory(self): with open('%s/fake_file.txt' % td, 'w') as f: self.assertTrue(self.fs.Exists(td)) + @unittest.skipIf(sys.version_info < (3, 4), "pathlib new in Python 3.4") + def test_fakepathlib(self): + with pathlib.Path('/fake_file.txt') as p: + with p.open('w') as f: + f.write('text') + is_windows = sys.platform.startswith('win') + if is_windows: + self.assertTrue(self.fs.Exists(r'\fake_file.txt')) + else: + self.assertTrue(self.fs.Exists('/fake_file.txt')) import math as path diff --git a/fake_pathlib_test.py b/fake_pathlib_test.py new file mode 100644 index 00000000..0d15d198 --- /dev/null +++ b/fake_pathlib_test.py @@ -0,0 +1,457 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unittests for fake_pathlib. +As most of fake_pathlib is a wrapper around fake_filesystem methods, the tests +are there mostly to ensure basic functionality. +Note that many of the tests are directly taken from examples in the python docs. +""" + +import os +import stat +import unittest + +import sys + +from pyfakefs import fake_filesystem +from pyfakefs import fake_pathlib + +is_windows = sys.platform == 'win32' + + +class FakePathlibInitializationTest(unittest.TestCase): + def setUp(self): + filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + filesystem.supports_drive_letter = False + self.pathlib = fake_pathlib.FakePathlibModule(filesystem) + self.path = self.pathlib.Path + + def test_initialization_type(self): + """Make sure tests for class type will work""" + path = self.path('/test') + if is_windows: + self.assertTrue(isinstance(path, self.pathlib.WindowsPath)) + self.assertTrue(isinstance(path, self.pathlib.PureWindowsPath)) + self.assertTrue(self.pathlib.PurePosixPath()) + self.assertRaises(NotImplementedError, self.pathlib.PosixPath) + else: + self.assertTrue(isinstance(path, self.pathlib.PosixPath)) + self.assertTrue(isinstance(path, self.pathlib.PurePosixPath)) + self.assertTrue(self.pathlib.PureWindowsPath()) + self.assertRaises(NotImplementedError, self.pathlib.WindowsPath) + + def test_init_with_segments(self): + """Basic initialization tests - taken from pathlib.Path documentation""" + self.assertEqual(self.path('/', 'foo', 'bar', 'baz'), + self.path('/foo/bar/baz')) + self.assertEqual(self.path(), self.path('.')) + self.assertEqual(self.path(self.path('foo'), self.path('bar')), + self.path('foo/bar')) + self.assertEqual(self.path('/etc') / 'init.d' / 'reboot', + self.path('/etc/init.d/reboot')) + + def test_init_collapse(self): + """Tests for collapsing path during initialization - taken from pathlib.PurePath documentation""" + self.assertEqual(self.path('foo//bar'), self.path('foo/bar')) + self.assertEqual(self.path('foo/./bar'), self.path('foo/bar')) + self.assertNotEqual(self.path('foo/../bar'), self.path('foo/bar')) + self.assertEqual(self.path('/etc', '/usr', 'lib64'), self.path('/usr/lib64')) + + def test_path_parts(self): + path = self.path('/foo/bar/setup.py') + self.assertEqual(path.parts, ('/', 'foo', 'bar', 'setup.py')) + self.assertEqual(path.drive, '') + self.assertEqual(path.root, '/') + self.assertEqual(path.anchor, '/') + self.assertEqual(path.name, 'setup.py') + self.assertEqual(path.stem, 'setup') + self.assertEqual(path.suffix, '.py') + self.assertEqual(path.parent, self.path('/foo/bar')) + self.assertEqual(path.parents[0], self.path('/foo/bar')) + self.assertEqual(path.parents[1], self.path('/foo')) + self.assertEqual(path.parents[2], self.path('/')) + + def test_is_absolute(self): + self.assertTrue(self.path('/a/b').is_absolute()) + self.assertFalse(self.path('a/b').is_absolute()) + + +class FakePathlibInitializationWithDriveTest(unittest.TestCase): + def setUp(self): + filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + filesystem.supports_drive_letter = True + pathlib = fake_pathlib.FakePathlibModule(filesystem) + self.path = pathlib.Path + + def test_init_with_segments(self): + """Basic initialization tests - taken from pathlib.Path documentation""" + self.assertEqual(self.path('c:/', 'foo', 'bar', 'baz'), self.path('c:/foo/bar/baz')) + self.assertEqual(self.path(), self.path('.')) + self.assertEqual(self.path(self.path('foo'), self.path('bar')), self.path('foo/bar')) + self.assertEqual(self.path('c:/Users') / 'john' / 'data', self.path('c:/Users/john/data')) + + def test_init_collapse(self): + """Tests for collapsing path during initialization - taken from pathlib.PurePath documentation""" + self.assertEqual(self.path('c:/Windows', 'd:bar'), self.path('d:bar')) + self.assertEqual(self.path('c:/Windows', '/Program Files'), self.path('c:/Program Files')) + + def test_path_parts(self): + path = self.path('d:/python scripts/setup.py') + self.assertEqual(path.parts, ('d:/', 'python scripts', 'setup.py')) + self.assertEqual(path.drive, 'd:') + self.assertEqual(path.root, '/') + self.assertEqual(path.anchor, 'd:/') + self.assertEqual(path.name, 'setup.py') + self.assertEqual(path.stem, 'setup') + self.assertEqual(path.suffix, '.py') + self.assertEqual(path.parent, self.path('d:/python scripts')) + self.assertEqual(path.parents[0], self.path('d:/python scripts')) + self.assertEqual(path.parents[1], self.path('d:/')) + + def test_is_absolute(self): + self.assertTrue(self.path('c:/a/b').is_absolute()) + self.assertFalse(self.path('/a/b').is_absolute()) + self.assertFalse(self.path('c:').is_absolute()) + self.assertTrue(self.path('//some/share').is_absolute()) + + +class FakePathlibPurePathTest(unittest.TestCase): + """Tests functionality present in PurePath class.""" + + def setUp(self): + filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + filesystem.supports_drive_letter = True + pathlib = fake_pathlib.FakePathlibModule(filesystem) + self.path = pathlib.Path + + def test_is_reserved(self): + self.assertFalse(self.path('/dev').is_reserved()) + self.assertFalse(self.path('/').is_reserved()) + if is_windows: + self.assertTrue(self.path('COM1').is_reserved()) + self.assertTrue(self.path('nul.txt').is_reserved()) + else: + self.assertFalse(self.path('COM1').is_reserved()) + self.assertFalse(self.path('nul.txt').is_reserved()) + + def test_joinpath(self): + self.assertEqual(self.path('/etc').joinpath('passwd'), + self.path('/etc/passwd')) + self.assertEqual(self.path('/etc').joinpath(self.path('passwd')), + self.path('/etc/passwd')) + self.assertEqual(self.path('/foo').joinpath('bar', 'baz'), + self.path('/foo/bar/baz')) + self.assertEqual(self.path('c:').joinpath('/Program Files'), + self.path('c:/Program Files')) + + def test_match(self): + self.assertTrue(self.path('a/b.py').match('*.py')) + self.assertTrue(self.path('/a/b/c.py').match('b/*.py')) + self.assertFalse(self.path('/a/b/c.py').match('a/*.py')) + self.assertTrue(self.path('/a.py').match('/*.py')) + self.assertFalse(self.path('a/b.py').match('/*.py')) + + def test_relative_to(self): + self.assertEqual(self.path('/etc/passwd').relative_to('/'), self.path('etc/passwd')) + self.assertEqual(self.path('/etc/passwd').relative_to('/'), self.path('etc/passwd')) + self.assertRaises(ValueError, self.path('passwd').relative_to, '/usr') + + def test_with_name(self): + self.assertEqual(self.path('c:/Downloads/pathlib.tar.gz').with_name('setup.py'), + self.path('c:/Downloads/setup.py')) + self.assertRaises(ValueError, self.path('c:/').with_name, 'setup.py') + + def test_with_suffix(self): + self.assertEqual(self.path('c:/Downloads/pathlib.tar.gz').with_suffix('.bz2'), + self.path('c:/Downloads/pathlib.tar.bz2')) + self.assertEqual(self.path('README').with_suffix('.txt'), + self.path('README.txt')) + + +class FakePathlibFileObjectPropertyTest(unittest.TestCase): + def setUp(self): + self.filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + self.filesystem.supports_drive_letter = False + pathlib = fake_pathlib.FakePathlibModule(self.filesystem) + self.path = pathlib.Path + self.filesystem.CreateFile('/home/jane/test.py', st_size=100, st_mode=stat.S_IFREG | 0o666) + self.filesystem.CreateDirectory('/home/john') + self.filesystem.CreateLink('/john', '/home/john') + self.filesystem.CreateLink('/test.py', '/home/jane/test.py') + self.filesystem.CreateLink('/broken_dir_link', '/home/none') + self.filesystem.CreateLink('/broken_file_link', '/home/none/test.py') + + def test_exists(self): + self.assertTrue(self.path('/home/jane/test.py').exists()) + self.assertTrue(self.path('/home/jane').exists()) + self.assertFalse(self.path('/home/jane/test').exists()) + self.assertTrue(self.path('/john').exists()) + self.assertTrue(self.path('/test.py').exists()) + self.assertFalse(self.path('/broken_dir_link').exists()) + self.assertFalse(self.path('/broken_file_link').exists()) + + def test_is_dir(self): + self.assertFalse(self.path('/home/jane/test.py').is_dir()) + self.assertTrue(self.path('/home/jane').is_dir()) + self.assertTrue(self.path('/john').is_dir()) + self.assertFalse(self.path('/test.py').is_dir()) + self.assertFalse(self.path('/broken_dir_link').is_dir()) + self.assertFalse(self.path('/broken_file_link').is_dir()) + + def test_is_file(self): + self.assertTrue(self.path('/home/jane/test.py').is_file()) + self.assertFalse(self.path('/home/jane').is_file()) + self.assertFalse(self.path('/john').is_file()) + self.assertTrue(self.path('/test.py').is_file()) + self.assertFalse(self.path('/broken_dir_link').is_file()) + self.assertFalse(self.path('/broken_file_link').is_file()) + + def test_is_symlink(self): + self.assertFalse(self.path('/home/jane/test.py').is_symlink()) + self.assertFalse(self.path('/home/jane').is_symlink()) + self.assertTrue(self.path('/john').is_symlink()) + self.assertTrue(self.path('/test.py').is_symlink()) + self.assertTrue(self.path('/broken_dir_link').is_symlink()) + self.assertTrue(self.path('/broken_file_link').is_symlink()) + + def test_stat(self): + file_object = self.filesystem.ResolveObject('/home/jane/test.py') + + stat_result = self.path('/test.py').stat() + self.assertFalse(stat_result[stat.ST_MODE] & stat.S_IFDIR) + self.assertTrue(stat_result[stat.ST_MODE] & stat.S_IFREG) + self.assertEqual(stat_result[stat.ST_INO], file_object.st_ino) + self.assertEqual(stat_result[stat.ST_SIZE], 100) + self.assertEqual(stat_result[stat.ST_MTIME], file_object.st_mtime) + + def test_lstat(self): + link_object = self.filesystem.LResolveObject('/test.py') + + stat_result = self.path('/test.py').lstat() + self.assertTrue(stat_result[stat.ST_MODE] & stat.S_IFREG) + self.assertTrue(stat_result[stat.ST_MODE] & stat.S_IFLNK) + self.assertEqual(stat_result[stat.ST_INO], link_object.st_ino) + self.assertEqual(stat_result[stat.ST_SIZE], len('/home/jane/test.py')) + self.assertEqual(stat_result[stat.ST_MTIME], link_object.st_mtime) + + def test_chmod(self): + file_object = self.filesystem.ResolveObject('/home/jane/test.py') + link_object = self.filesystem.LResolveObject('/test.py') + self.path('/test.py').chmod(0o444) + self.assertEqual(file_object.st_mode, stat.S_IFREG | 0o444) + self.assertEqual(link_object.st_mode, stat.S_IFLNK | 0o777) + + def test_lchmod(self): + file_object = self.filesystem.ResolveObject('/home/jane/test.py') + link_object = self.filesystem.LResolveObject('/test.py') + if not hasattr(os, "lchmod"): + self.assertRaises(NotImplementedError, self.path('/test.py').lchmod, 0o444) + else: + self.path('/test.py').lchmod(0o444) + self.assertEqual(file_object.st_mode, stat.S_IFREG | 0o666) + self.assertEqual(link_object.st_mode, stat.S_IFLNK | 0o444) + + def test_resolve(self): + self.filesystem.cwd = '/home/antoine' + self.filesystem.CreateDirectory('/home/antoine/docs') + self.filesystem.CreateFile('/home/antoine/setup.py') + self.assertEqual(self.path().resolve(), + self.path('/home/antoine')) + self.assertEqual(self.path('docs/../setup.py').resolve(), + self.path('/home/antoine/setup.py')) + + def test_cwd(self): + self.filesystem.cwd = '/home/jane' + self.assertEqual(self.path.cwd(), self.path('/home/jane')) + + def test_expanduser(self): + if is_windows: + self.assertEqual(self.path('~').expanduser(), + self.path(os.environ['USERPROFILE'].replace('\\', '/'))) + else: + self.assertEqual(self.path('~').expanduser(), + self.path(os.environ['HOME'])) + + def test_home(self): + if is_windows: + self.assertEqual(self.path.home(), + self.path(os.environ['USERPROFILE'].replace('\\', '/'))) + else: + self.assertEqual(self.path.home(), + self.path(os.environ['HOME'])) + + +class FakePathlibPathFileOperationTest(unittest.TestCase): + """Tests methods related to file and directory handling.""" + + def setUp(self): + self.filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + self.filesystem.supports_drive_letter = False + self.filesystem.is_case_sensitive = True + pathlib = fake_pathlib.FakePathlibModule(self.filesystem) + self.path = pathlib.Path + + def test_exists(self): + self.filesystem.CreateFile('/home/jane/test.py') + self.filesystem.CreateDirectory('/home/john') + self.filesystem.CreateLink('/john', '/home/john') + self.filesystem.CreateLink('/none', '/home/none') + + self.assertTrue(self.path('/home/jane/test.py').exists()) + self.assertTrue(self.path('/home/jane').exists()) + self.assertTrue(self.path('/john').exists()) + self.assertFalse(self.path('/none').exists()) + self.assertFalse(self.path('/home/jane/test').exists()) + + def test_open(self): + self.filesystem.CreateDirectory('/foo') + self.assertRaises(OSError, self.path('/foo/bar.txt').open) + self.path('/foo/bar.txt').open('w') + self.assertTrue(self.filesystem.Exists('/foo/bar.txt')) + + @unittest.skipIf(sys.version_info < (3, 5), 'New in version 3.5') + def test_read_text(self): + self.filesystem.CreateFile('text_file', contents='ерунда', encoding='cyrillic') + file_path = self.path('text_file') + self.assertEqual(file_path.read_text(encoding='cyrillic'), 'ерунда') + + @unittest.skipIf(sys.version_info < (3, 5), 'New in version 3.5') + def test_write_text(self): + file_path = self.path('text_file') + file_path.write_text('ανοησίες', encoding='greek') + self.assertTrue(self.filesystem.Exists('text_file')) + file_object = self.filesystem.ResolveObject('text_file') + self.assertEqual(file_object.byte_contents.decode('greek'), 'ανοησίες') + + @unittest.skipIf(sys.version_info < (3, 5), 'New in version 3.5') + def test_read_bytes(self): + self.filesystem.CreateFile('binary_file', contents=b'Binary file contents') + file_path = self.path('binary_file') + self.assertEqual(file_path.read_bytes(), b'Binary file contents') + + @unittest.skipIf(sys.version_info < (3, 5), 'New in version 3.5') + def test_write_bytes(self): + file_path = self.path('binary_file') + file_path.write_bytes(b'Binary file contents') + self.assertTrue(self.filesystem.Exists('binary_file')) + file_object = self.filesystem.ResolveObject('binary_file') + self.assertEqual(file_object.byte_contents, b'Binary file contents') + + def test_rename(self): + self.filesystem.CreateFile('/foo/bar.txt', contents='test') + self.path('/foo/bar.txt').rename('foo/baz.txt') + self.assertFalse(self.filesystem.Exists('/foo/bar.txt')) + file_obj = self.filesystem.ResolveObject('foo/baz.txt') + self.assertTrue(file_obj) + self.assertEqual(file_obj.contents, 'test') + + def test_replace(self): + self.filesystem.CreateFile('/foo/bar.txt', contents='test') + self.filesystem.CreateFile('/bar/old.txt', contents='replaced') + self.path('/bar/old.txt').replace('foo/bar.txt') + self.assertFalse(self.filesystem.Exists('/bar/old.txt')) + file_obj = self.filesystem.ResolveObject('foo/bar.txt') + self.assertTrue(file_obj) + self.assertEqual(file_obj.contents, 'replaced') + + def test_unlink(self): + self.filesystem.CreateFile('/foo/bar.txt', contents='test') + self.assertTrue(self.filesystem.Exists('/foo/bar.txt')) + self.path('/foo/bar.txt').unlink() + self.assertFalse(self.filesystem.Exists('/foo/bar.txt')) + + def test_touch_non_existing(self): + self.filesystem.CreateDirectory('/foo') + self.path('/foo/bar.txt').touch(mode=0o444) + file_obj = self.filesystem.ResolveObject('/foo/bar.txt') + self.assertTrue(file_obj) + self.assertEqual(file_obj.contents, '') + self.assertTrue(file_obj.st_mode, stat.S_IFREG | 0o444) + + def test_touch_existing(self): + self.filesystem.CreateFile('/foo/bar.txt', contents='test') + file_path = self.path('/foo/bar.txt') + self.assertRaises(FileExistsError, file_path.touch, exist_ok=False) + file_path.touch() + file_obj = self.filesystem.ResolveObject('/foo/bar.txt') + self.assertTrue(file_obj) + self.assertEqual(file_obj.contents, 'test') + + def test_samefile(self): + self.filesystem.CreateFile('/foo/bar.txt') + self.filesystem.CreateFile('/foo/baz.txt') + self.assertRaises(OSError, self.path('/foo/other').samefile, '/foo/other.txt') + path = self.path('/foo/bar.txt') + self.assertRaises(OSError, path.samefile, '/foo/other.txt') + self.assertRaises(OSError, path.samefile, self.path('/foo/other.txt')) + self.assertFalse(path.samefile('/foo/baz.txt')) + self.assertFalse(path.samefile(self.path('/foo/baz.txt'))) + self.assertTrue(path.samefile('/foo/../foo/bar.txt')) + self.assertTrue(path.samefile(self.path('/foo/../foo/bar.txt'))) + + def test_symlink_to(self): + self.filesystem.CreateFile('/foo/bar.txt') + path = self.path('/link_to_bar') + path.symlink_to('/foo/bar.txt') + self.assertTrue(self.filesystem.Exists('/link_to_bar')) + file_obj = self.filesystem.ResolveObject('/foo/bar.txt') + linked_file_obj = self.filesystem.ResolveObject('/link_to_bar') + self.assertEqual(file_obj, linked_file_obj) + link__obj = self.filesystem.LResolveObject('/link_to_bar') + self.assertTrue(path.is_symlink()) + + def test_mkdir(self): + self.assertRaises(FileNotFoundError, self.path('/foo/bar').mkdir) + self.path('/foo/bar').mkdir(parents=True) + self.assertTrue(self.filesystem.Exists('/foo/bar')) + self.assertRaises(FileExistsError, self.path('/foo/bar').mkdir) + + @unittest.skipIf(sys.version_info < (3, 5), 'exist_ok argument new in Python 3.5') + def test_mkdir_exist_ok(self): + self.filesystem.CreateDirectory('/foo/bar') + self.path('foo/bar').mkdir(exist_ok=True) + self.filesystem.CreateFile('/foo/bar/baz') + self.assertRaises(FileExistsError, self.path('/foo/bar/baz').mkdir, exist_ok=True) + + def test_rmdir(self): + self.filesystem.CreateDirectory('/foo/bar') + self.path('/foo/bar').rmdir() + self.assertFalse(self.filesystem.Exists('/foo/bar')) + self.assertTrue(self.filesystem.Exists('/foo')) + self.filesystem.CreateFile('/foo/baz') + self.assertRaises(OSError, self.path('/foo').rmdir) + self.assertTrue(self.filesystem.Exists('/foo')) + + def test_iterdir(self): + self.filesystem.CreateFile('/foo/bar/file1') + self.filesystem.CreateFile('/foo/bar/file2') + self.filesystem.CreateFile('/foo/bar/file3') + path = self.path('/foo/bar') + contents = [entry for entry in path.iterdir()] + self.assertEqual(3, len(contents)) + self.assertIn(self.path('/foo/bar/file2'), contents) + + def test_glob(self): + self.filesystem.CreateFile('/foo/setup.py') + self.filesystem.CreateFile('/foo/all_tests.py') + self.filesystem.CreateFile('/foo/README.md') + self.filesystem.CreateFile('/foo/setup.pyc') + path = self.path('/foo') + self.assertEqual(sorted(path.glob('*.py')), + [self.path('/foo/all_tests.py'), self.path('/foo/setup.py')]) + +if __name__ == '__main__': + unittest.main() diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 6fc6de4b..b45082ea 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -179,7 +179,7 @@ def __init__(self, name, st_mode=stat.S_IFREG | PERM_DEF_FILE, Args: name: name of the file/directory, without parent path information st_mode: the stat.S_IF* constant representing the file type (i.e. - stat.S_IFREG, stat.SIFDIR) + stat.S_IFREG, stat.S_IFDIR) contents: the contents of the filesystem object; should be a string or byte object for regular files, and a list of other FakeFile or FakeDirectory objects for FakeDirectory objects @@ -621,6 +621,88 @@ def ChangeDiskUsage(self, usage_change, file_path, st_dev): file_path) mount_point['used_size'] += usage_change + def GetStat(self, entry_path, follow_symlinks=True): + """Returns the os.stat-like tuple for the FakeFile object of entry_path. + + Args: + entry_path: path to filesystem object to retrieve + follow_symlinks: if False and entry_path points to a link, the link itself is inspected + instead of the linked object + + Returns: + the os.stat_result object corresponding to entry_path + + Raises: + OSError: if the filesystem object doesn't exist. + """ + # stat should return the tuple representing return value of os.stat + try: + stats = self.ResolveObject(entry_path, follow_symlinks) + st_obj = os.stat_result((stats.st_mode, stats.st_ino, stats.st_dev, + stats.st_nlink, stats.st_uid, stats.st_gid, + stats.st_size, stats.st_atime, + stats.st_mtime, stats.st_ctime)) + return st_obj + except IOError as io_error: + raise OSError(io_error.errno, io_error.strerror, entry_path) + + def ChangeMode(self, path, mode, follow_symlinks=True): + """Change the permissions of a file as encoded in integer mode. + + Args: + path: (str) Path to the file. + mode: (int) Permissions + follow_symlinks: if False and entry_path points to a link, the link itself is affected + instead of the linked object + """ + try: + file_object = self.ResolveObject(path, follow_symlinks) + except IOError as io_error: + if io_error.errno == errno.ENOENT: + raise OSError(errno.ENOENT, + 'No such file or directory in fake filesystem', + path) + raise + file_object.st_mode = ((file_object.st_mode & ~PERM_ALL) | + (mode & PERM_ALL)) + file_object.st_ctime = time.time() + + def UpdateTime(self, path, times, follow_symlinks=True): + """Change the access and modified times of a file. + + Args: + path: (str) Path to the file. + times: 2-tuple of numbers, of the form (atime, mtime) which is used to set + the access and modified times, respectively. If None, file's access + and modified times are set to the current time. + follow_symlinks: if False and entry_path points to a link, the link itself is queried + instead of the linked object + + Raises: + TypeError: If anything other than integers is specified in passed tuple or + number of elements in the tuple is not equal to 2. + """ + try: + file_object = self.ResolveObject(path, follow_symlinks) + except IOError as io_error: + if io_error.errno == errno.ENOENT: + raise OSError(errno.ENOENT, + 'No such file or directory in fake filesystem', + path) + raise + if times is None: + file_object.st_atime = time.time() + file_object.st_mtime = time.time() + else: + if len(times) != 2: + raise TypeError('utime() arg 2 must be a tuple (atime, mtime)') + for t in times: + if not isinstance(t, (int, float)): + raise TypeError('atime and mtime must be numbers') + + file_object.st_atime = times[0] + file_object.st_mtime = times[1] + def SetIno(self, path, st_ino): """Set the self.st_ino attribute of file at 'path'. @@ -1176,11 +1258,12 @@ def GetObject(self, file_path): file_path = self.NormalizePath(self.NormalizeCase(file_path)) return self.GetObjectFromNormalizedPath(file_path) - def ResolveObject(self, file_path): + def ResolveObject(self, file_path, follow_symlinks=True): """Searches for the specified filesystem object, resolving all links. Args: file_path: specifies target FakeFile object to retrieve + follow_symlinks: if False, the link itself is resolved, other wise the object linked to Returns: the FakeFile object corresponding to file_path @@ -1188,7 +1271,9 @@ def ResolveObject(self, file_path): Raises: IOError: if the object is not found """ - return self.GetObjectFromNormalizedPath(self.ResolvePath(file_path)) + if follow_symlinks: + return self.GetObjectFromNormalizedPath(self.ResolvePath(file_path)) + return self.LResolveObject(file_path) def LResolveObject(self, path): """Searches for the specified object, resolving only parent links. @@ -1242,13 +1327,15 @@ def AddObject(self, file_path, file_object): 'Not a directory in the fake filesystem', file_path) - def RenameObject(self, old_file, new_file): + def RenameObject(self, old_file, new_file, force_replace=False): """Renames a FakeFile object at old_file to new_file, preserving all properties. Also replaces existing new_file object, if one existed (Unix only). Args: old_file: path to filesystem object to rename new_file: path to where the filesystem object will live after this call + force_replace: if set and destination is an existing file, it will be replaced + even under Windows if the user has permissions Raises: OSError: - if old_file does not exist @@ -1273,7 +1360,7 @@ def RenameObject(self, old_file, new_file): raise OSError(errno.EEXIST, 'Fake filesystem object: can not rename to existing directory', new_file) - elif _is_windows: + elif _is_windows and not force_replace: raise OSError(errno.EEXIST, 'Fake filesystem object: can not rename to existing file', new_file) @@ -1422,12 +1509,13 @@ def CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE, return file_object - def CreateLink(self, file_path, link_target): + def CreateLink(self, file_path, link_target, target_is_directory=False): """Creates the specified symlink, pointed at the specified link target. Args: file_path: path to the symlink to create link_target: the target of the symlink + target_is_directory: ignored, here to satisfy pathlib API Returns: the newly created FakeFile object @@ -1486,6 +1574,155 @@ def CreateHardLink(self, old_path, new_path): self.AddObject(new_parent_directory, old_file) return old_file + def MakeDirectory(self, dir_name, mode=PERM_DEF): + """Create a leaf Fake directory. + + Args: + dir_name: (str) Name of directory to create. Relative paths are assumed + to be relative to '/'. + mode: (int) Mode to create directory with. This argument defaults to + 0o777. The umask is applied to this mode. + + Raises: + OSError: if the directory name is invalid or parent directory is read only + or as per FakeFilesystem.AddObject. + """ + if self._EndsWithPathSeparator(dir_name): + dir_name = dir_name[:-1] + + parent_dir, _ = self.SplitPath(dir_name) + if parent_dir: + base_dir = self.CollapsePath(parent_dir) + if parent_dir.endswith(self.path_separator + '..'): + base_dir, unused_dotdot, _ = parent_dir.partition(self.path_separator + '..') + if not self.Exists(base_dir): + raise OSError(errno.ENOENT, 'No such fake directory', base_dir) + + dir_name = self.NormalizePath(dir_name) + if self.Exists(dir_name): + raise OSError(errno.EEXIST, 'Fake object already exists', dir_name) + head, tail = self.SplitPath(dir_name) + directory_object = self.GetObject(head) + if not directory_object.st_mode & PERM_WRITE: + raise OSError(errno.EACCES, 'Permission Denied', dir_name) + + self.AddObject( + head, FakeDirectory(tail, mode & ~self.umask)) + + def MakeDirectories(self, dir_name, mode=PERM_DEF, exist_ok=False): + """Create a leaf Fake directory + create any non-existent parent dirs. + + Args: + dir_name: (str) Name of directory to create. + mode: (int) Mode to create directory (and any necessary parent + directories) with. This argument defaults to 0o777. The umask is + applied to this mode. + exist_ok: (boolean) If exist_ok is False (the default), an OSError is + raised if the target directory already exists. New in Python 3.2. + + Raises: + OSError: if the directory already exists and exist_ok=False, or as per + FakeFilesystem.CreateDirectory() + """ + dir_name = self.NormalizePath(dir_name) + path_components = self.GetPathComponents(dir_name) + + # Raise a permission denied error if the first existing directory is not + # writeable. + current_dir = self.root + for component in path_components: + if component not in current_dir.contents: + if not current_dir.st_mode & PERM_WRITE: + raise OSError(errno.EACCES, 'Permission Denied', dir_name) + else: + break + else: + current_dir = current_dir.contents[component] + try: + self.CreateDirectory(dir_name, mode & ~self.umask) + except OSError: + if (not exist_ok or + not isinstance(self.ResolveObject(dir_name), FakeDirectory)): + raise + + def _ConfirmDir(self, target_directory): + """Tests that the target is actually a directory, raising OSError if not. + + Args: + target_directory: path to the target directory within the fake + filesystem + + Returns: + the FakeFile object corresponding to target_directory + + Raises: + OSError: if the target is not a directory + """ + try: + directory = self.GetObject(target_directory) + except IOError as e: + raise OSError(e.errno, e.strerror, target_directory) + if not directory.st_mode & stat.S_IFDIR: + raise OSError(errno.ENOTDIR, + 'Fake os module: not a directory', + target_directory) + return directory + + def RemoveFile(self, path): + """Removes the FakeFile object representing the specified file.""" + path = self.NormalizePath(path) + if self.Exists(path): + obj = self.ResolveObject(path) + if stat.S_IFMT(obj.st_mode) == stat.S_IFDIR: + link_obj = self.LResolveObject(path) + if stat.S_IFMT(link_obj.st_mode) != stat.S_IFLNK: + raise OSError(errno.EISDIR, "Is a directory: '%s'" % path) + + try: + self.RemoveObject(path) + except IOError as e: + raise OSError(e.errno, e.strerror, e.filename) + + def RemoveDirectory(self, target_directory): + """Remove a leaf Fake directory. + + Args: + target_directory: (str) Name of directory to remove. + + Raises: + OSError: if target_directory does not exist or is not a directory, + or as per FakeFilesystem.RemoveObject. Cannot remove '.'. + """ + if target_directory == '.': + raise OSError(errno.EINVAL, 'Invalid argument: \'.\'') + target_directory = self.NormalizePath(target_directory) + if self._ConfirmDir(target_directory): + dir_object = self.ResolveObject(target_directory) + if dir_object.contents: + raise OSError(errno.ENOTEMPTY, 'Fake Directory not empty', + target_directory) + try: + self.RemoveObject(target_directory) + except IOError as e: + raise OSError(e.errno, e.strerror, e.filename) + + def ListDir(self, target_directory): + """Returns a sorted list of filenames in target_directory. + + Args: + target_directory: path to the target directory within the fake + filesystem + + Returns: + a sorted list of file names within the target directory + + Raises: + OSError: if the target is not a directory + """ + target_directory = self.ResolvePath(target_directory) + directory = self._ConfirmDir(target_directory) + return sorted(directory.contents) + def __str__(self): return str(self.root) @@ -1896,29 +2133,6 @@ def fstat(self, file_des): stats.st_mtime, stats.st_ctime)) return st_obj - def _ConfirmDir(self, target_directory): - """Tests that the target is actually a directory, raising OSError if not. - - Args: - target_directory: path to the target directory within the fake - filesystem - - Returns: - the FakeFile object corresponding to target_directory - - Raises: - OSError: if the target is not a directory - """ - try: - directory = self.filesystem.GetObject(target_directory) - except IOError as e: - raise OSError(e.errno, e.strerror, target_directory) - if not directory.st_mode & stat.S_IFDIR: - raise OSError(errno.ENOTDIR, - 'Fake os module: not a directory', - target_directory) - return directory - def umask(self, new_mask): """Change the current umask. @@ -1948,7 +2162,7 @@ def chdir(self, target_directory): the target is not a directory """ target_directory = self.filesystem.ResolvePath(target_directory) - self._ConfirmDir(target_directory) + self.filesystem._ConfirmDir(target_directory) directory = self.filesystem.GetObject(target_directory) # A full implementation would check permissions all the way up the tree. if not directory.st_mode | PERM_EXE: @@ -1979,9 +2193,7 @@ def listdir(self, target_directory): Raises: OSError: if the target is not a directory """ - target_directory = self.filesystem.ResolvePath(target_directory) - directory = self._ConfirmDir(target_directory) - return sorted(directory.contents) + return self.filesystem.ListDir(target_directory) if sys.version_info >= (3, 5): class DirEntry(): @@ -2054,7 +2266,7 @@ def scandir(self, path=''): OSError: if the target is not a directory """ path = self.filesystem.ResolvePath(path) - fake_dir = self._ConfirmDir(path) + fake_dir = self.filesystem._ConfirmDir(path) for entry in fake_dir.contents: dir_entry = self.DirEntry(self) dir_entry.name = entry @@ -2152,11 +2364,13 @@ def readlink(self, path): raise OSError(errno.EINVAL, 'Fake os module: not a symlink', path) return link_obj.contents - def stat(self, entry_path): + def stat(self, entry_path, follow_symlinks=None): """Returns the os.stat-like tuple for the FakeFile object of entry_path. Args: entry_path: path to filesystem object to retrieve + follow_symlinks: if False and entry_path points to a link, the link itself is inspected + instead of the linked object. New in Python 3.3. Returns: the os.stat_result object corresponding to entry_path @@ -2164,16 +2378,11 @@ def stat(self, entry_path): Raises: OSError: if the filesystem object doesn't exist. """ - # stat should return the tuple representing return value of os.stat - try: - stats = self.filesystem.ResolveObject(entry_path) - st_obj = os.stat_result((stats.st_mode, stats.st_ino, stats.st_dev, - stats.st_nlink, stats.st_uid, stats.st_gid, - stats.st_size, stats.st_atime, - stats.st_mtime, stats.st_ctime)) - return st_obj - except IOError as io_error: - raise OSError(io_error.errno, io_error.strerror, entry_path) + if follow_symlinks is None: + follow_symlinks = True + elif sys.version_info < (3, 3): + raise TypeError("stat() got an unexpected keyword argument 'follow_symlinks'") + return self.filesystem.GetStat(entry_path, follow_symlinks) def lstat(self, entry_path): """Returns the os.stat-like tuple for entry_path, not following symlinks. @@ -2188,25 +2397,11 @@ def lstat(self, entry_path): OSError: if the filesystem object doesn't exist. """ # stat should return the tuple representing return value of os.stat - try: - stats = self.filesystem.LResolveObject(entry_path) - st_obj = os.stat_result((stats.st_mode, stats.st_ino, stats.st_dev, - stats.st_nlink, stats.st_uid, stats.st_gid, - stats.st_size, stats.st_atime, - stats.st_mtime, stats.st_ctime)) - return st_obj - except IOError as io_error: - raise OSError(io_error.errno, io_error.strerror, entry_path) + return self.filesystem.GetStat(entry_path, follow_symlinks=False) def remove(self, path): """Removes the FakeFile object representing the specified file.""" - path = self.filesystem.NormalizePath(path) - if self.path.isdir(path) and not self.path.islink(path): - raise OSError(errno.EISDIR, "Is a directory: '%s'" % path) - try: - self.filesystem.RemoveObject(path) - except IOError as e: - raise OSError(e.errno, e.strerror, e.filename) + self.filesystem.RemoveFile(path) # As per the documentation unlink = remove. unlink = remove @@ -2229,6 +2424,25 @@ def rename(self, old_file, new_file): """ self.filesystem.RenameObject(old_file, new_file) + + if sys.version_info >= (3, 3): + def replace(self, old_file, new_file): + """Renames a FakeFile object at old_file to new_file, preserving all properties. + Also replaces existing new_file object, if one existed (both Windows and Unix). + + Args: + old_file: path to filesystem object to rename + new_file: path to where the filesystem object will live after this call + + Raises: + OSError: - if old_file does not exist + - new_file is an existing directory, + - if new_file is an existing file and could not be removed (Unix) + - if dirname(new_file) does not exist + - if the file would be moved to another filesystem (e.g. mount point) + """ + self.filesystem.RenameObject(old_file, new_file, force_replace=True) + def rmdir(self, target_directory): """Remove a leaf Fake directory. @@ -2239,22 +2453,12 @@ def rmdir(self, target_directory): OSError: if target_directory does not exist or is not a directory, or as per FakeFilesystem.RemoveObject. Cannot remove '.'. """ - if target_directory == '.': - raise OSError(errno.EINVAL, 'Invalid argument: \'.\'') - target_directory = self.filesystem.NormalizePath(target_directory) - if self._ConfirmDir(target_directory): - if self.listdir(target_directory): - raise OSError(errno.ENOTEMPTY, 'Fake Directory not empty', - target_directory) - try: - self.filesystem.RemoveObject(target_directory) - except IOError as e: - raise OSError(e.errno, e.strerror, e.filename) + self.filesystem.RemoveDirectory(target_directory) def removedirs(self, target_directory): """Remove a leaf Fake directory and all empty intermediate ones.""" target_directory = self.filesystem.NormalizePath(target_directory) - directory = self._ConfirmDir(target_directory) + directory = self.filesystem._ConfirmDir(target_directory) if directory.contents: raise OSError(errno.ENOTEMPTY, 'Fake Directory not empty', self.path.basename(target_directory)) @@ -2264,7 +2468,7 @@ def removedirs(self, target_directory): if not tail: head, tail = self.path.split(head) while head and tail: - head_dir = self._ConfirmDir(head) + head_dir = self.filesystem._ConfirmDir(head) if head_dir.contents: break self.rmdir(head) @@ -2283,29 +2487,9 @@ def mkdir(self, dir_name, mode=PERM_DEF): OSError: if the directory name is invalid or parent directory is read only or as per FakeFilesystem.AddObject. """ - if self.filesystem._EndsWithPathSeparator(dir_name): - dir_name = dir_name[:-1] + self.filesystem.MakeDirectory(dir_name, mode) - parent_dir, _ = self.path.split(dir_name) - if parent_dir: - base_dir = self.path.normpath(parent_dir) - if parent_dir.endswith(self.sep + '..'): - base_dir, unused_dotdot, _ = parent_dir.partition(self.sep + '..') - if not self.filesystem.Exists(base_dir): - raise OSError(errno.ENOENT, 'No such fake directory', base_dir) - - dir_name = self.filesystem.NormalizePath(dir_name) - if self.filesystem.Exists(dir_name): - raise OSError(errno.EEXIST, 'Fake object already exists', dir_name) - head, tail = self.path.split(dir_name) - directory_object = self.filesystem.GetObject(head) - if not directory_object.st_mode & PERM_WRITE: - raise OSError(errno.EACCES, 'Permission Denied', dir_name) - - self.filesystem.AddObject( - head, FakeDirectory(tail, mode & ~self.filesystem.umask)) - - def makedirs(self, dir_name, mode=PERM_DEF, exist_ok=False): + def makedirs(self, dir_name, mode=PERM_DEF, exist_ok=None): """Create a leaf Fake directory + create any non-existent parent dirs. Args: @@ -2320,67 +2504,61 @@ def makedirs(self, dir_name, mode=PERM_DEF, exist_ok=False): OSError: if the directory already exists and exist_ok=False, or as per FakeFilesystem.CreateDirectory() """ - if exist_ok and sys.version_info < (3, 2): + if exist_ok is None: + exist_ok = False + elif sys.version_info < (3, 2): raise TypeError("makedir() got an unexpected keyword argument 'exist_ok'") + self.filesystem.MakeDirectories(dir_name, mode, exist_ok) - dir_name = self.filesystem.NormalizePath(dir_name) - path_components = self.filesystem.GetPathComponents(dir_name) - - # Raise a permission denied error if the first existing directory is not - # writeable. - current_dir = self.filesystem.root - for component in path_components: - if component not in current_dir.contents: - if not current_dir.st_mode & PERM_WRITE: - raise OSError(errno.EACCES, 'Permission Denied', dir_name) - else: - break - else: - current_dir = current_dir.contents[component] - try: - self.filesystem.CreateDirectory(dir_name, mode & ~self.filesystem.umask) - except OSError: - if not exist_ok or not self.path.isdir(dir_name): - raise - - def access(self, path, mode): + def access(self, path, mode, follow_symlinks=None): """Check if a file exists and has the specified permissions. Args: path: (str) Path to the file. mode: (int) Permissions represented as a bitwise-OR combination of os.F_OK, os.R_OK, os.W_OK, and os.X_OK. + follow_symlinks: if False and entry_path points to a link, the link itself is queried + instead of the linked object. New in Python 3.3. Returns: boolean, True if file is accessible, False otherwise """ + if follow_symlinks is not None and sys.version_info < (3, 3): + raise TypeError("access() got an unexpected keyword argument 'follow_symlinks'") try: - st = self.stat(path) + st = self.stat(path, follow_symlinks) except OSError as os_error: if os_error.errno == errno.ENOENT: return False raise return (mode & ((st.st_mode >> 6) & 7)) == mode - def chmod(self, path, mode): + def chmod(self, path, mode, follow_symlinks=None): """Change the permissions of a file as encoded in integer mode. Args: path: (str) Path to the file. mode: (int) Permissions + follow_symlinks: if False and entry_path points to a link, the link itself is changed + instead of the linked object. New in Python 3.3. """ - try: - file_object = self.filesystem.GetObject(path) - except IOError as io_error: - if io_error.errno == errno.ENOENT: - raise OSError(errno.ENOENT, - 'No such file or directory in fake filesystem', - path) - raise - file_object.st_mode = ((file_object.st_mode & ~PERM_ALL) | - (mode & PERM_ALL)) - file_object.st_ctime = time.time() + if follow_symlinks is None: + follow_symlinks = True + elif sys.version_info < (3, 3): + raise TypeError("chmod() got an unexpected keyword argument 'follow_symlinks'") + self.filesystem.ChangeMode(path, mode, follow_symlinks) + + if not _is_windows: + def lchmod(self, path, mode): + """Change the permissions of a file as encoded in integer mode. + If the file is a link, the permissions of the link are changed. - def utime(self, path, times): + Args: + path: (str) Path to the file. + mode: (int) Permissions + """ + self.filesystem.ChangeMode(path, mode, follow_symlinks=False) + + def utime(self, path, times, follow_symlinks=None): """Change the access and modified times of a file. Args: @@ -2388,45 +2566,38 @@ def utime(self, path, times): times: 2-tuple of numbers, of the form (atime, mtime) which is used to set the access and modified times, respectively. If None, file's access and modified times are set to the current time. + follow_symlinks: if False and entry_path points to a link, the link itself is queried + instead of the linked object. New in Python 3.3. Raises: TypeError: If anything other than integers is specified in passed tuple or number of elements in the tuple is not equal to 2. """ - try: - file_object = self.filesystem.ResolveObject(path) - except IOError as io_error: - if io_error.errno == errno.ENOENT: - raise OSError(errno.ENOENT, - 'No such file or directory in fake filesystem', - path) - raise - if times is None: - file_object.st_atime = time.time() - file_object.st_mtime = time.time() - else: - if len(times) != 2: - raise TypeError('utime() arg 2 must be a tuple (atime, mtime)') - for t in times: - if not isinstance(t, (int, float)): - raise TypeError('atime and mtime must be numbers') - - file_object.st_atime = times[0] - file_object.st_mtime = times[1] + if follow_symlinks is None: + follow_symlinks = True + elif sys.version_info < (3, 3): + raise TypeError("utime() got an unexpected keyword argument 'follow_symlinks'") + self.filesystem.UpdateTime(path, times, follow_symlinks) - def chown(self, path, uid, gid): + def chown(self, path, uid, gid, follow_symlinks=None): """Set ownership of a faked file. Args: path: (str) Path to the file or directory. uid: (int) Numeric uid to set the file or directory to. gid: (int) Numeric gid to set the file or directory to. + follow_symlinks: if False and entry_path points to a link, the link itself is changed + instead of the linked object. New in Python 3.3. `None` is also allowed for `uid` and `gid`. This permits `os.rename` to use `os.chown` even when the source file `uid` and `gid` are `None` (unset). """ + if follow_symlinks is None: + follow_symlinks = True + elif sys.version_info < (3, 3): + raise TypeError("chown() got an unexpected keyword argument 'follow_symlinks'") try: - file_object = self.filesystem.GetObject(path) + file_object = self.filesystem.ResolveObject(path, follow_symlinks) except IOError as io_error: if io_error.errno == errno.ENOENT: raise OSError(errno.ENOENT, diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index 45f1eca7..f5800c2b 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -50,6 +50,8 @@ from pyfakefs import fake_filesystem from pyfakefs import fake_filesystem_shutil from pyfakefs import fake_tempfile +if sys.version_info >= (3, 4): + from pyfakefs import fake_pathlib if sys.version_info < (3,): import __builtin__ as builtins @@ -134,9 +136,13 @@ class Patcher(object): ''' assert None in SKIPMODULES, "sys.modules contains 'None' values; must skip them." + HAS_PATHLIB = sys.version_info >= (3, 4) + # To add py.test support per issue https://github.com/jmcgeheeiv/pyfakefs/issues/43, # it appears that adding 'py', 'pytest', '_pytest' to SKIPNAMES will help SKIPNAMES = set(['os', 'path', 'tempfile', 'io']) + if HAS_PATHLIB: + SKIPNAMES.add('pathlib') def __init__(self, additional_skip_names=None, patch_path=True): """For a description of the arguments, see TestCase.__init__""" @@ -151,6 +157,8 @@ def __init__(self, additional_skip_names=None, patch_path=True): # Attributes set by _findModules() self._osModules = None self._pathModules = None + if self.HAS_PATHLIB: + self._pathlibModules = None self._shutilModules = None self._tempfileModules = None self._ioModules = None @@ -163,6 +171,8 @@ def __init__(self, additional_skip_names=None, patch_path=True): self.fs = None self.fake_os = None self.fake_path = None + if self.HAS_PATHLIB: + self.fake_pathlib = None self.fake_shutil = None self.fake_tempfile_ = None self.fake_open = None @@ -181,6 +191,8 @@ def _findModules(self): """ self._osModules = set() self._pathModules = set() + if self.HAS_PATHLIB: + self._pathlibModules = set() self._shutilModules = set() self._tempfileModules = set() self._ioModules = set() @@ -193,6 +205,8 @@ def _findModules(self): self._osModules.add(module) if self._patchPath and 'path' in module.__dict__: self._pathModules.add(module) + if self.HAS_PATHLIB and 'pathlib' in module.__dict__: + self._pathlibModules.add(module) if 'shutil' in module.__dict__: self._shutilModules.add(module) if 'tempfile' in module.__dict__: @@ -209,6 +223,8 @@ def _refresh(self): self.fs = fake_filesystem.FakeFilesystem() self.fake_os = fake_filesystem.FakeOsModule(self.fs) self.fake_path = self.fake_os.path + if self.HAS_PATHLIB: + self.fake_pathlib = fake_pathlib.FakePathlibModule(self.fs) self.fake_shutil = fake_filesystem_shutil.FakeShutilModule(self.fs) self.fake_tempfile_ = fake_tempfile.FakeTempfileModule(self.fs) self.fake_open = fake_filesystem.FakeFileOpen(self.fs) @@ -220,8 +236,7 @@ def setUp(self, doctester=None): """Bind the file-related modules to the :py:mod:`pyfakefs` fake modules real ones. Also bind the fake `file()` and `open()` functions. """ - if self._isStale: - self._refresh() + self._refresh() if doctester is not None: doctester.globs = self.replaceGlobs(doctester.globs) @@ -235,6 +250,9 @@ def setUp(self, doctester=None): self._stubs.SmartSet(module, 'os', self.fake_os) for module in self._pathModules: self._stubs.SmartSet(module, 'path', self.fake_path) + if self.HAS_PATHLIB: + for module in self._pathlibModules: + self._stubs.SmartSet(module, 'pathlib', self.fake_pathlib) for module in self._shutilModules: self._stubs.SmartSet(module, 'shutil', self.fake_shutil) for module in self._tempfileModules: diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py new file mode 100644 index 00000000..e8477378 --- /dev/null +++ b/pyfakefs/fake_pathlib.py @@ -0,0 +1,548 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A fake implementation for pathlib working with FakeFilesystem. +Usage: +* With fake_filesystem_unittest: + If using fake_filesystem_unittest.TestCase, pathlib gets replaced + by fake_pathlib together with other file system related modules. + +* Stand-alone with FakeFilesystem: + `filesystem = fake_filesystem.FakeFilesystem()` + `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)` + `path = fake_pathlib_module.Path('/foo/bar')` + +Note: as the implementation is based on FakeFilesystem, all faked classes +(including PurePosixPath, PosixPath, PureWindowsPath and WindowsPath) +get the properties of the underlying fake filesystem. +""" + +import os +import pathlib +from urllib.parse import quote_from_bytes as urlquote_from_bytes + +import sys + +import functools + +from pyfakefs.fake_filesystem import FakeFileOpen, FakeFilesystem + + +def init_module(filesystem): + """Initializes the fake module with the fake file system.""" + # pylint: disable=protected-access + FakePath.filesystem = filesystem + FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour(filesystem) + FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem) + + +def _wrap_strfunc(strfunc): + @functools.wraps(strfunc) + def _wrapped(pathobj, *args): + return strfunc(pathobj.filesystem, str(pathobj), *args) + + return staticmethod(_wrapped) + + +def _wrap_binary_strfunc(strfunc): + @functools.wraps(strfunc) + def _wrapped(pathobj1, pathobj2, *args): + return strfunc(pathobj1.filesystem, str(pathobj1), str(pathobj2), *args) + + return staticmethod(_wrapped) + + +def _wrap_binary_strfunc_reverse(strfunc): + @functools.wraps(strfunc) + def _wrapped(pathobj1, pathobj2, *args): + return strfunc(pathobj2.filesystem, str(pathobj2), str(pathobj1), *args) + + return staticmethod(_wrapped) + + +class _FakeAccessor(pathlib._Accessor): # pylint: disable=protected-access + """Accessor which forwards some of the functions to FakeFilesystem methods.""" + + stat = _wrap_strfunc(FakeFilesystem.GetStat) + + lstat = _wrap_strfunc(lambda fs, path: FakeFilesystem.GetStat(fs, path, follow_symlinks=False)) + + listdir = _wrap_strfunc(FakeFilesystem.ListDir) + + chmod = _wrap_strfunc(FakeFilesystem.ChangeMode) + + if hasattr(os, "lchmod"): + lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.ChangeMode( + fs, path, mode, follow_symlinks=False)) + else: + def lchmod(self, pathobj, mode): + """Raises not implemented for Windows systems.""" + raise NotImplementedError("lchmod() not available on this system") + + mkdir = _wrap_strfunc(FakeFilesystem.MakeDirectory) + + unlink = _wrap_strfunc(FakeFilesystem.RemoveFile) + + rmdir = _wrap_strfunc(FakeFilesystem.RemoveDirectory) + + rename = _wrap_binary_strfunc(FakeFilesystem.RenameObject) + + replace = _wrap_binary_strfunc(lambda fs, old_path, new_path: + FakeFilesystem.RenameObject( + fs, old_path, new_path, force_replace=True)) + + symlink = _wrap_binary_strfunc_reverse(FakeFilesystem.CreateLink) + + utime = _wrap_strfunc(FakeFilesystem.UpdateTime) + + +_fake_accessor = _FakeAccessor() + + +class _FakeFlavour(pathlib._Flavour): + """Fake Flavour implementation used by PurePath and _Flavour""" + + filesystem = None + sep = '/' + altsep = None + has_drv = False + + ext_namespace_prefix = '\\\\?\\' + + drive_letters = ( + set(chr(x) for x in range(ord('a'), ord('z') + 1)) | + set(chr(x) for x in range(ord('A'), ord('Z') + 1)) + ) + + def __init__(self, filesystem): + self.filesystem = filesystem + self.sep = filesystem.path_separator + self.altsep = filesystem.alternative_path_separator + self.has_drv = filesystem.supports_drive_letter + super(_FakeFlavour, self).__init__() + + @staticmethod + def _split_extended_path(path, ext_prefix=ext_namespace_prefix): + prefix = '' + if path.startswith(ext_prefix): + prefix = path[:4] + path = path[4:] + if path.startswith('UNC\\'): + prefix += path[:3] + path = '\\' + path[3:] + return prefix, path + + def _splitroot_with_drive(self, path, sep): + first = path[0:1] + second = path[1:2] + if second == sep and first == sep: + # XXX extended paths should also disable the collapsing of "." + # components (according to MSDN docs). + prefix, path = self._split_extended_path(path) + first = path[0:1] + second = path[1:2] + else: + prefix = '' + third = path[2:3] + if second == sep and first == sep and third != sep: + # is a UNC path: + # vvvvvvvvvvvvvvvvvvvvv root + # \\machine\mountpoint\directory\etc\... + # directory ^^^^^^^^^^^^^^ + index = path.find(sep, 2) + if index != -1: + index2 = path.find(sep, index + 1) + # a UNC path can't have two slashes in a row + # (after the initial two) + if index2 != index + 1: + if index2 == -1: + index2 = len(path) + if prefix: + return prefix + path[1:index2], sep, path[index2 + 1:] + else: + return path[:index2], sep, path[index2 + 1:] + drv = root = '' + if second == ':' and first in self.drive_letters: + drv = path[:2] + path = path[2:] + first = third + if first == sep: + root = first + path = path.lstrip(sep) + return prefix + drv, root, path + + @staticmethod + def _splitroot_posix(path, sep): + if path and path[0] == sep: + stripped_part = path.lstrip(sep) + if len(path) - len(stripped_part) == 2: + return '', sep * 2, stripped_part + else: + return '', sep, stripped_part + else: + return '', '', path + + def splitroot(self, path, sep=None): + """Split path into drive, root and rest.""" + if sep is None: + sep = self.filesystem.path_separator + if self.filesystem.supports_drive_letter: + return self._splitroot_with_drive(path, sep) + return self._splitroot_posix(path, sep) + + def casefold(self, s): + """Return the lower-case version of s for a case-insensitive filesystem.""" + if self.filesystem.is_case_sensitive: + return s + return s.lower() + + def casefold_parts(self, parts): + """Return the lower-case version of parts for a case-insensitive filesystem.""" + if self.filesystem.is_case_sensitive: + return parts + return [p.lower() for p in parts] + + def resolve(self, path): + """Make the path absolute, resolving any symlinks.""" + return self.filesystem.ResolvePath(str(path)) + + def gethomedir(self, username): + """Return the home directory of the current user.""" + if not username: + try: + return os.environ['HOME'] + except KeyError: + import pwd + return pwd.getpwuid(os.getuid()).pw_dir + else: + import pwd + try: + return pwd.getpwnam(username).pw_dir + except KeyError: + raise RuntimeError("Can't determine home directory " + "for %r" % username) + + +class _FakeWindowsFlavour(_FakeFlavour): + """Flavour used by PureWindowsPath with some Windows specific implementations + independent of FakeFilesystem properties. + """ + reserved_names = ( + {'CON', 'PRN', 'AUX', 'NUL'} | + {'COM%d' % i for i in range(1, 10)} | + {'LPT%d' % i for i in range(1, 10)} + ) + + def is_reserved(self, parts): + """Return True if the path is considered reserved under Windows.""" + + # NOTE: the rules for reserved names seem somewhat complicated + # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). + # We err on the side of caution and return True for paths which are + # not considered reserved by Windows. + if not parts: + return False + if self.filesystem.supports_drive_letter and parts[0].startswith('\\\\'): + # UNC paths are never reserved + return False + return parts[-1].partition('.')[0].upper() in self.reserved_names + + def make_uri(self, path): + """Return a file URI for the given path""" + + # Under Windows, file URIs use the UTF-8 encoding. + # original version, not faked + # todo: make this part dependent on drive support, add encoding as property + drive = path.drive + if len(drive) == 2 and drive[1] == ':': + # It's a path on a local drive => 'file:///c:/a/b' + rest = path.as_posix()[2:].lstrip('/') + return 'file:///%s/%s' % ( + drive, urlquote_from_bytes(rest.encode('utf-8'))) + else: + # It's a path on a network drive => 'file://host/share/a/b' + return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) + + def gethomedir(self, username): + """Return the home directory of the current user.""" + + # original version, not faked + if 'HOME' in os.environ: + userhome = os.environ['HOME'] + elif 'USERPROFILE' in os.environ: + userhome = os.environ['USERPROFILE'] + elif 'HOMEPATH' in os.environ: + try: + drv = os.environ['HOMEDRIVE'] + except KeyError: + drv = '' + userhome = drv + os.environ['HOMEPATH'] + else: + raise RuntimeError("Can't determine home directory") + + if username: + # Try to guess user home directory. By default all users + # directories are located in the same place and are named by + # corresponding usernames. If current user home directory points + # to nonstandard place, this guess is likely wrong. + if os.environ['USERNAME'] != username: + drv, root, parts = self.parse_parts((userhome,)) + if parts[-1] != os.environ['USERNAME']: + raise RuntimeError("Can't determine home directory " + "for %r" % username) + parts[-1] = username + if drv or root: + userhome = drv + root + self.join(parts[1:]) + else: + userhome = self.join(parts) + return userhome + + +class _FakePosixFlavour(_FakeFlavour): + """Flavour used by PurePosixPath with some Unix specific implementations + independent of FakeFilesystem properties. + """ + + def is_reserved(self, parts): + return False + + def make_uri(self, path): + # We represent the path using the local filesystem encoding, + # for portability to other applications. + bpath = bytes(path) + return 'file://' + urlquote_from_bytes(bpath) + + def gethomedir(self, username): + # original version, not faked + if not username: + try: + return os.environ['HOME'] + except KeyError: + import pwd + return pwd.getpwuid(os.getuid()).pw_dir + else: + import pwd + try: + return pwd.getpwnam(username).pw_dir + except KeyError: + raise RuntimeError("Can't determine home directory " + "for %r" % username) + + +class FakePath(pathlib.Path): + """Replacement for pathlib.Path. Reimplement some methods to use fake filesystem. + The rest of the methods work as they are, as they will use the fake accessor. + """ + + # the underlying fake filesystem + filesystem = None + + def __new__(cls, *args, **kwargs): + """Creates the correct subclass based on OS.""" + if cls is FakePathlibModule.Path: + cls = FakePathlibModule.WindowsPath if os.name == 'nt' else FakePathlibModule.PosixPath + self = cls._from_parts(args, init=True) + return self + + def _path(self): + """Returns the underlying path string as used by the fake filesystem.""" + return str(self) + + def _init(self, template=None): + """Initializer called from base class.""" + self._accessor = _fake_accessor + self._closed = False + + @classmethod + def cwd(cls): + """Return a new path pointing to the current working directory + (as returned by os.getcwd()). + """ + return cls(cls.filesystem.cwd) + + @classmethod + def home(cls): + """Return a new path pointing to the user's home directory (as + returned by os.path.expanduser('~')). + """ + return cls(cls()._flavour.gethomedir(None). + replace(os.sep, cls.filesystem.path_separator)) + + def samefile(self, other_path): + """Return whether other_path is the same or not as this file + (as returned by os.path.samefile()). + + Args: + other_path: a path object or string of the file object to be compared with + + Raises: + OSError: if the filesystem object doesn't exist. + """ + st = self.stat() + try: + other_st = other_path.stat() + except AttributeError: + other_st = self.filesystem.GetStat(other_path) + return st.st_ino == other_st.st_ino and st.st_dev == other_st.st_dev + + def resolve(self): + """Make the path absolute, resolving all symlinks on the way and also + normalizing it (for example turning slashes into backslashes under Windows). + + Raises: + IOError: if the path doesn't exist + """ + if self._closed: + self._raise_closed() + path = self.filesystem.ResolvePath(self._path()) + return FakePath(path) + + def open(self, mode='r', buffering=-1, encoding=None, + errors=None, newline=None): + """Open the file pointed by this path and return a fake file object. + + Raises: + IOError: if the target object is a directory, the path is invalid or + permission is denied. + """ + if self._closed: + self._raise_closed() + return FakeFileOpen(self.filesystem)( + self._path(), mode, buffering, encoding, errors, newline) + + if sys.version_info >= (3, 5): + def read_bytes(self): + """Open the fake file in bytes mode, read it, and close the file. + + Raises: + IOError: if the target object is a directory, the path is invalid or + permission is denied. + """ + with FakeFileOpen(self.filesystem)(self._path(), mode='rb') as f: + return f.read() + + def read_text(self, encoding=None, errors=None): + """ + Open the fake file in text mode, read it, and close the file. + """ + with FakeFileOpen(self.filesystem)( + self._path(), mode='r', encoding=encoding, errors=errors) as f: + return f.read() + + def write_bytes(self, data): + """Open the fake file in bytes mode, write to it, and close the file. + Args: + data: the bytes to be written + Raises: + IOError: if the target object is a directory, the path is invalid or + permission is denied. + """ + # type-check for the buffer interface before truncating the file + view = memoryview(data) + with FakeFileOpen(self.filesystem)(self._path(), mode='wb') as f: + return f.write(view) + + def write_text(self, data, encoding=None, errors=None): + """Open the fake file in text mode, write to it, and close the file. + + Args: + data: the string to be written + encoding: the encoding used for the string; if not given, the + default locale encoding is used + errors: ignored + Raises: + TypeError: if data is not of type 'str' + IOError: if the target object is a directory, the path is invalid or + permission is denied. + """ + if not isinstance(data, str): + raise TypeError('data must be str, not %s' % + data.__class__.__name__) + with FakeFileOpen(self.filesystem)( + self._path(), mode='w', encoding=encoding, errors=errors) as f: + return f.write(data) + + def touch(self, mode=0o666, exist_ok=True): + """Create a fake file for the path with the given access mode, if it doesn't exist. + + Args: + mode: the file mode for the file if it does not exist + exist_ok: if the file already exists and this is True, nothinh happens, + otherwise FileExistError is raised + + Raises: + FileExistsError if the file exists and exits_ok is False. + """ + if self._closed: + self._raise_closed() + if self.exists(): + if exist_ok: + self.filesystem.UpdateTime(self._path(), None) + else: + raise FileExistsError + else: + fake_file = self.open('w') + fake_file.close() + self.chmod(mode) + + def expanduser(self): + """ Return a new path with expanded ~ and ~user constructs + (as returned by os.path.expanduser) + """ + return FakePath(os.path.expanduser(self._path()) + .replace(os.path.sep, self.filesystem.path_separator)) + + +class FakePathlibModule(object): + """Uses FakeFilesystem to provide a fake pathlib module replacement. + + You need a fake_filesystem to use this: + `filesystem = fake_filesystem.FakeFilesystem()` + `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)` + """ + + def __init__(self, filesystem): + """ + Initializes the module with the given filesystem. + + Args: + filesystem: FakeFilesystem used to provide file system information + """ + init_module(filesystem) + self._pathlib_module = pathlib + + class PurePosixPath(pathlib.PurePath): + """A subclass of PurePath, that represents non-Windows filesystem paths""" + __slots__ = () + + class PureWindowsPath(pathlib.PurePath): + """A subclass of PurePath, that represents Windows filesystem paths""" + __slots__ = () + + if sys.platform == 'win32': + class WindowsPath(FakePath, PureWindowsPath): + """A subclass of Path and PureWindowsPath that represents + concrete Windows filesystem paths. + """ + __slots__ = () + else: + class PosixPath(FakePath, PurePosixPath): + """A subclass of Path and PurePosixPath that represents + concrete non-Windows filesystem paths. + """ + __slots__ = () + + Path = FakePath + + def __getattr__(self, name): + """Forwards any unfaked calls to the standard pathlib module.""" + return getattr(self._pathlib_module, name) From e6feac85bb11108537d63750268aed4189d92368 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Sun, 6 Nov 2016 17:36:24 +0100 Subject: [PATCH 04/10] Fixed some pylint warnings - increased max line length to 100 to avoid too many warnings - removed invalid pylint codes, replaced pylint codes with names for better readability - adapted release notes --- .pylintrc | 5 +- CHANGES.md | 6 +- fake_tempfile_test.py | 2 +- pyfakefs/fake_filesystem.py | 309 +++++++++++++++++---------- pyfakefs/fake_filesystem_glob.py | 71 +++--- pyfakefs/fake_filesystem_shutil.py | 20 +- pyfakefs/fake_filesystem_unittest.py | 95 ++++---- pyfakefs/fake_tempfile.py | 108 +++++----- 8 files changed, 360 insertions(+), 256 deletions(-) diff --git a/.pylintrc b/.pylintrc index 2f947e92..a4740bd6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -194,8 +194,9 @@ max-nested-blocks=5 [FORMAT] -# Maximum number of characters on a single line as per Google coding style. -max-line-length=80 +# Maximum number of characters on a single line +# increased from 80 to 100 +max-line-length=100 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ diff --git a/CHANGES.md b/CHANGES.md index 5d6c816c..bd4006de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,13 @@ The release versions are PyPi releases. ## Unreleased #### New Features + * support for `pathlib` (Python >= 3.4) + * support for `os.replace` (Python >= 3.3) + * `os.access`, `os.chmod`, `os.chown`, `os.stat`, `os.utime`: + support for `follow_symlinks` argument (Python >= 3.3) * support for `os.scandir` (Python >= 3.5) * option to not fake modules named `path` - * `glob.glob`, `glob.iglob`: support for `recursive` argument + * `glob.glob`, `glob.iglob`: support for `recursive` argument (Python >= 3.5) * support for `glob.iglob` ## Version 2.9 diff --git a/fake_tempfile_test.py b/fake_tempfile_test.py index 7178c834..910381b4 100755 --- a/fake_tempfile_test.py +++ b/fake_tempfile_test.py @@ -103,7 +103,7 @@ def testTempFilenamePrefix(self): def testTempFilenameDir(self): """test tempfile._TempFilename(dir=).""" - filename = self.tempfile._TempFilename(dir='/dir') + filename = self.tempfile._TempFilename(directory='/dir') self.assertTrue(filename.startswith('/dir/tmp')) self.assertLess(len('/dir/tmpX'), len(filename)) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index b45082ea..008ea0f3 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -12,30 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# pylint: disable-msg=W0612,W0613,C6409 """A fake filesystem implementation for unit testing. -Includes: - FakeFile: Provides the appearance of a real file. - FakeDirectory: Provides the appearance of a real dir. - FakeFilesystem: Provides the appearance of a real directory hierarchy. - FakeOsModule: Uses FakeFilesystem to provide a fake os module replacement. - FakePathModule: Faked os.path module replacement. - FakeFileOpen: Faked file() and open() function replacements. +:Includes: + * FakeFile: Provides the appearance of a real file. + * FakeDirectory: Provides the appearance of a real dir. + * FakeFilesystem: Provides the appearance of a real directory hierarchy. + * FakeOsModule: Uses FakeFilesystem to provide a fake os module replacement. + * FakePathModule: Faked os.path module replacement. + * FakeFileOpen: Faked file() and open() function replacements. -Usage: +:Usage: >>> from pyfakefs import fake_filesystem >>> filesystem = fake_filesystem.FakeFilesystem() >>> os_module = fake_filesystem.FakeOsModule(filesystem) >>> pathname = '/a/new/dir/new-file' Create a new file object, creating parent directory objects as needed: + >>> os_module.path.exists(pathname) False >>> new_file = filesystem.CreateFile(pathname) File objects can't be overwritten: + >>> os_module.path.exists(pathname) True >>> try: @@ -45,17 +46,20 @@ ... assert e.strerror == 'File already exists in fake filesystem' Remove a file object: + >>> filesystem.RemoveObject(pathname) >>> os_module.path.exists(pathname) False Create a new file object at the previous path: + >>> beatles_file = filesystem.CreateFile(pathname, ... contents='Dear Prudence\\nWon\\'t you come out to play?\\n') >>> os_module.path.exists(pathname) True Use the FakeFileOpen class to read fake file objects: + >>> file_module = fake_filesystem.FakeFileOpen(filesystem) >>> for line in file_module(pathname): ... print line.rstrip() @@ -64,6 +68,7 @@ Won't you come out to play? File objects cannot be treated like directory objects: + >>> os_module.listdir(pathname) #doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): File "fake_filesystem.py", line 291, in listdir @@ -71,10 +76,12 @@ OSError: [Errno 20] Fake os module: not a directory: '/a/new/dir/new-file' The FakeOsModule can list fake directory objects: + >>> os_module.listdir(os_module.path.dirname(pathname)) ['new-file'] The FakeOsModule also supports stat operations: + >>> import stat >>> stat.S_ISREG(os_module.stat(pathname).st_mode) True @@ -87,14 +94,17 @@ import io import locale import os -import stat import sys import time import warnings + from collections import namedtuple +import stat + + if sys.version_info < (3, 0): - import cStringIO + import cStringIO # pylint: disable=import-error __pychecker__ = 'no-reimportself' @@ -125,25 +135,24 @@ 'FakeOsModule docstring for details.') -class Error(Exception): - pass - - -_is_windows = sys.platform.startswith('win') -_is_cygwin = sys.platform == 'cygwin' +_IS_WINDOWS = sys.platform.startswith('win') +_IS_CYGWIN = sys.platform == 'cygwin' # Python 3.2 supports links in Windows -_is_link_supported = not _is_windows or sys.version_info >= (3, 2) +_IS_LINK_SUPPORTED = not _IS_WINDOWS or sys.version_info >= (3, 2) -if _is_windows: +if _IS_WINDOWS: # On Windows, raise WindowsError instead of OSError if available - OSError = WindowsError # pylint: disable-msg=E0602,W0622 + OSError = WindowsError # pylint: disable=undefined-variable,redefined-builtin -class FakeLargeFileIoException(Error): +class FakeLargeFileIoException(Exception): + """Exception thrown on unsupported operations for fake large files. + Fake large files have a size with no real content. + """ def __init__(self, file_path): - Error.__init__(self, - 'Read and write operations not supported for ' - 'fake large file: %s' % file_path) + super(FakeLargeFileIoException, self).__init__( + 'Read and write operations not supported for ' + 'fake large file: %s' % file_path) def CopyModule(old): @@ -202,40 +211,45 @@ def __init__(self, name, st_mode=stat.S_IFREG | PERM_DEF_FILE, # Non faked features, write setter methods for faking them self.st_uid = None self.st_gid = None - # shall be set on creating the file from the file system to get access to fs available space @property def contents(self): - # returns the byte contents as ACSII string - for testing convenience + """Return the byte contents as ACSII string (for testing convenience).""" if sys.version_info >= (3, 0) and isinstance(self.byte_contents, bytes): return self.byte_contents.decode('ascii') return self.byte_contents @property def st_ctime(self): + """Return the creation time of the fake file.""" return (self._st_ctime if FakeOsModule.stat_float_times() else int(self._st_ctime)) @property def st_atime(self): + """Return the access time of the fake file.""" return (self._st_atime if FakeOsModule.stat_float_times() else int(self._st_atime)) @property def st_mtime(self): + """Return the modification time of the fake file.""" return (self._st_mtime if FakeOsModule.stat_float_times() else int(self._st_mtime)) @st_ctime.setter def st_ctime(self, val): + """Set the creation time of the fake file.""" self._st_ctime = val @st_atime.setter def st_atime(self, val): + """Set the access time of the fake file.""" self._st_atime = val @st_mtime.setter def st_mtime(self, val): + """Set the modification time of the fake file.""" self._st_mtime = val def SetLargeFileSize(self, st_size): @@ -252,7 +266,7 @@ def SetLargeFileSize(self, st_size): or if st_size exceeds the available file system space """ # the st_size should be an positive integer value - int_types = (int, long) if sys.version_info < (3, 0) else int + int_types = (int, long) if sys.version_info < (3, 0) else int # pylint: disable=undefined-variable if not isinstance(st_size, int_types) or st_size < 0: raise IOError(errno.ENOSPC, 'Fake file object: can not create non negative integer ' @@ -269,7 +283,9 @@ def IsLargeFile(self): """Return True if this file was initialized with size but no contents.""" return self.byte_contents is None - def _EncodeContents(self, contents, encoding=None): + @staticmethod + def _EncodeContents(contents, encoding=None): + # pylint: disable=undefined-variable if sys.version_info >= (3, 0) and isinstance(contents, str): contents = bytes(contents, encoding or locale.getpreferredencoding(False)) elif sys.version_info < (3, 0) and isinstance(contents, unicode): @@ -344,7 +360,8 @@ def SetSize(self, st_size): self.byte_contents = self.byte_contents[:st_size] else: if sys.version_info < (3, 0): - self.byte_contents = '%s%s' % (self.byte_contents, '\0' * (st_size - current_size)) + self.byte_contents = '%s%s' % ( + self.byte_contents, '\0' * (st_size - current_size)) else: self.byte_contents += b'\0' * (st_size - current_size) self.st_size = st_size @@ -433,8 +450,9 @@ def RemoveEntry(self, pathname_name, recursive=True): """ entry = self.contents[pathname_name] if entry.st_mode & PERM_WRITE == 0: - raise OSError(errno.EACCES, 'Trying to remove object without write permission', pathname_name) - if _is_windows and self.filesystem and self.filesystem.HasOpenFile(entry): + raise OSError(errno.EACCES, 'Trying to remove object without write permission', + pathname_name) + if _IS_WINDOWS and self.filesystem and self.filesystem.HasOpenFile(entry): raise OSError(errno.EACCES, 'Trying to remove an open file', pathname_name) if recursive and isinstance(entry, FakeDirectory): while entry.contents: @@ -452,13 +470,13 @@ def GetSize(self): return sum([item[1].GetSize() for item in self.contents.items()]) def __str__(self): - rc = super(FakeDirectory, self).__str__() + ':\n' + description = super(FakeDirectory, self).__str__() + ':\n' for item in self.contents: item_desc = self.contents[item].__str__() for line in item_desc.split('\n'): if line: - rc = rc + ' ' + line + '\n' - return rc + description = description + ' ' + line + '\n' + return description class FakeFilesystem(object): @@ -478,8 +496,8 @@ def __init__(self, path_separator=os.path.sep, total_size=None): self.alternative_path_separator = os.path.altsep if path_separator != os.sep: self.alternative_path_separator = None - self.is_case_sensitive = not _is_windows and sys.platform != 'darwin' - self.supports_drive_letter = _is_windows + self.is_case_sensitive = not _IS_WINDOWS and sys.platform != 'darwin' + self.supports_drive_letter = _IS_WINDOWS self.root = FakeDirectory(self.path_separator, filesystem=self) self.cwd = self.root.name # We can't query the current value without changing it: @@ -594,8 +612,8 @@ def SetDiskUsage(self, total_size, path=None): mount_point = self._MountPointForPath(path) if mount_point['total_size'] is not None and mount_point['used_size'] > total_size: raise IOError(errno.ENOSPC, - 'Fake file system: cannot change size to %r bytes - used space is larger' % total_size, - path) + 'Fake file system: cannot change size to %r bytes - ' + 'used space is larger' % total_size, path) mount_point['total_size'] = total_size def ChangeDiskUsage(self, usage_change, file_path, st_dev): @@ -617,8 +635,8 @@ def ChangeDiskUsage(self, usage_change, file_path, st_dev): if mount_point['total_size'] is not None: if mount_point['total_size'] - mount_point['used_size'] < usage_change: raise IOError(errno.ENOSPC, - 'Fake file system: disk is full, failed to add %r bytes' % usage_change, - file_path) + 'Fake file system: disk is full, failed to add %r bytes' + % usage_change, file_path) mount_point['used_size'] += usage_change def GetStat(self, entry_path, follow_symlinks=True): @@ -696,8 +714,8 @@ def UpdateTime(self, path, times, follow_symlinks=True): else: if len(times) != 2: raise TypeError('utime() arg 2 must be a tuple (atime, mtime)') - for t in times: - if not isinstance(t, (int, float)): + for file_time in times: + if not isinstance(file_time, (int, float)): raise TypeError('atime and mtime must be numbers') file_object.st_atime = times[0] @@ -758,7 +776,7 @@ def GetOpenFile(self, file_des): if not isinstance(file_des, int): raise TypeError('an integer is required') if (file_des >= len(self.open_files) or - self.open_files[file_des] is None): + self.open_files[file_des] is None): raise OSError(errno.EBADF, 'Bad file descriptor', file_des) return self.open_files[file_des] @@ -815,7 +833,7 @@ def CollapsePath(self, path): continue if component == '..': if collapsed_path_components and ( - collapsed_path_components[-1] != '..'): + collapsed_path_components[-1] != '..'): # Remove an up-reference: directory/.. collapsed_path_components.pop() continue @@ -829,6 +847,15 @@ def CollapsePath(self, path): return drive + collapsed_path or '.' def NormalizeCase(self, path): + """Return a normalized case version of the given path for case-insensitive + file systems. For case-sensitive file systems, return path unchanged. + + Args: + path: the file path to be transformed + + Returns: + A version of path matching the case of existing path elements. + """ if self.is_case_sensitive or not path: return path path_components = self.GetPathComponents(path) @@ -836,11 +863,13 @@ def NormalizeCase(self, path): current_dir = self.root for component in path_components: dir_name, current_dir = self._DirectoryContent(current_dir, component) - if current_dir is None or current_dir.byte_contents is None and current_dir.st_size == 0: + if current_dir is None or ( + current_dir.byte_contents is None and current_dir.st_size == 0): return path normalized_components.append(dir_name) normalized_path = self.path_separator.join(normalized_components) - if path.startswith(self.path_separator) and not normalized_path.startswith(self.path_separator): + if path.startswith(self.path_separator) and not normalized_path.startswith( + self.path_separator): normalized_path = self.path_separator + normalized_path return normalized_path @@ -901,16 +930,18 @@ def SplitPath(self, path): return (drive or self.path_separator, basename) def SplitDrive(self, path): - """Splits the path into the drive part and the rest of the path, if drive letters are supported - and a drive is present, otherwise returns an empty string and the original path. - Taken from Windows specific implementation in Python 3.5 and slightly adapted. + """Splits the path into the drive part and the rest of the path, if drive letters + are supported and a drive is present, otherwise returns an empty string and the + original path. + Taken from Windows specific implementation in Python 3.5 and slightly adapted. """ if self.supports_drive_letter: if len(path) >= 2: path = self.NormalizePathSeparator(path) # UNC path handling is here since Python 2.7.8, back-ported from Python 3 if sys.version_info >= (2, 7, 8): - if (path[0:2] == self.path_separator * 2) and (path[2:3] != self.path_separator): + if (path[0:2] == self.path_separator * 2) and ( + path[2:3] != self.path_separator): # UNC path handling - splits off the mount point instead of the drive sep_index = path.find(self.path_separator, 2) if sep_index == -1: @@ -1021,18 +1052,22 @@ def GetPathComponents(self, path): path_components.insert(0, drive) return path_components - def _StartsWithDriveLetter(self, file_path): - return self.supports_drive_letter and len(file_path) >= 2 and file_path[0].isalpha and file_path[1] == ':' + def StartsWithDriveLetter(self, file_path): + """Return True if file_path starts with a drive letter + and drive letter support is enabled in the filesystem.""" + return (self.supports_drive_letter and len(file_path) >= 2 and + file_path[0].isalpha and file_path[1] == ':') def _StartsWithRootPath(self, file_path): return (file_path.startswith(self.root.name) or - not self.is_case_sensitive and file_path.lower().startswith(self.root.name.lower()) or - self._StartsWithDriveLetter(file_path)) + not self.is_case_sensitive and file_path.lower().startswith( + self.root.name.lower()) or + self.StartsWithDriveLetter(file_path)) def _IsRootPath(self, file_path): return (file_path == self.root.name or not self.is_case_sensitive and file_path.lower() == self.root.name.lower() or - len(file_path) == 2 and self._StartsWithDriveLetter(file_path)) + len(file_path) == 2 and self.StartsWithDriveLetter(file_path)) def _EndsWithPathSeparator(self, file_path): return file_path and (file_path.endswith(self.path_separator) @@ -1043,7 +1078,8 @@ def _DirectoryContent(self, directory, component): if component in directory.contents: return component, directory.contents[component] if not self.is_case_sensitive: - matching_content = [(subdir, directory.contents[subdir]) for subdir in directory.contents + matching_content = [(subdir, directory.contents[subdir]) for subdir in + directory.contents if subdir.lower() == component.lower()] if matching_content: return matching_content[0] @@ -1360,15 +1396,15 @@ def RenameObject(self, old_file, new_file, force_replace=False): raise OSError(errno.EEXIST, 'Fake filesystem object: can not rename to existing directory', new_file) - elif _is_windows and not force_replace: + elif _IS_WINDOWS and not force_replace: raise OSError(errno.EEXIST, 'Fake filesystem object: can not rename to existing file', new_file) else: try: self.RemoveObject(new_file) - except IOError as e: - raise OSError(e.errno, e.strerror, e.filename) + except IOError as exc: + raise OSError(exc.errno, exc.strerror, exc.filename) old_dir, old_name = os.path.split(old_file) new_dir, new_name = os.path.split(new_file) @@ -1440,13 +1476,13 @@ def CreateDirectory(self, directory_path, perm_bits=PERM_DEF): current_dir = self.root for component in path_components: - dir = self._DirectoryContent(current_dir, component)[1] - if not dir: + directory = self._DirectoryContent(current_dir, component)[1] + if not directory: new_dir = FakeDirectory(component, perm_bits, filesystem=self) current_dir.AddEntry(new_dir) current_dir = new_dir else: - current_dir = dir + current_dir = directory self.last_ino += 1 current_dir.SetIno(self.last_ino) @@ -1509,6 +1545,7 @@ def CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE, return file_object + # pylint: disable=unused-argument def CreateLink(self, file_path, link_target, target_is_directory=False): """Creates the specified symlink, pointed at the specified link target. @@ -1524,7 +1561,7 @@ def CreateLink(self, file_path, link_target, target_is_directory=False): IOError: if the file already exists OSError: if on Windows before Python 3.2 """ - if not _is_link_supported: + if not _IS_LINK_SUPPORTED: raise OSError("Symbolic links are not supported on Windows before Python 3.2") resolved_file_path = self.ResolvePath(file_path) return self.CreateFile(resolved_file_path, st_mode=stat.S_IFLNK | PERM_DEF, @@ -1545,7 +1582,7 @@ def CreateHardLink(self, old_path, new_path): OSError: if the parent directory doesn't exist OSError: if on Windows before Python 3.2 """ - if not _is_link_supported: + if not _IS_LINK_SUPPORTED: raise OSError("Links are not supported on Windows before Python 3.2") new_path_normalized = self.NormalizePath(new_path) if self.Exists(new_path_normalized): @@ -1594,7 +1631,7 @@ def MakeDirectory(self, dir_name, mode=PERM_DEF): if parent_dir: base_dir = self.CollapsePath(parent_dir) if parent_dir.endswith(self.path_separator + '..'): - base_dir, unused_dotdot, _ = parent_dir.partition(self.path_separator + '..') + base_dir, dummy_dotdot, _ = parent_dir.partition(self.path_separator + '..') if not self.Exists(base_dir): raise OSError(errno.ENOENT, 'No such fake directory', base_dir) @@ -1645,7 +1682,7 @@ def MakeDirectories(self, dir_name, mode=PERM_DEF, exist_ok=False): not isinstance(self.ResolveObject(dir_name), FakeDirectory)): raise - def _ConfirmDir(self, target_directory): + def ConfirmDir(self, target_directory): """Tests that the target is actually a directory, raising OSError if not. Args: @@ -1660,8 +1697,8 @@ def _ConfirmDir(self, target_directory): """ try: directory = self.GetObject(target_directory) - except IOError as e: - raise OSError(e.errno, e.strerror, target_directory) + except IOError as exc: + raise OSError(exc.errno, exc.strerror, target_directory) if not directory.st_mode & stat.S_IFDIR: raise OSError(errno.ENOTDIR, 'Fake os module: not a directory', @@ -1680,8 +1717,8 @@ def RemoveFile(self, path): try: self.RemoveObject(path) - except IOError as e: - raise OSError(e.errno, e.strerror, e.filename) + except IOError as exc: + raise OSError(exc.errno, exc.strerror, exc.filename) def RemoveDirectory(self, target_directory): """Remove a leaf Fake directory. @@ -1696,15 +1733,15 @@ def RemoveDirectory(self, target_directory): if target_directory == '.': raise OSError(errno.EINVAL, 'Invalid argument: \'.\'') target_directory = self.NormalizePath(target_directory) - if self._ConfirmDir(target_directory): + if self.ConfirmDir(target_directory): dir_object = self.ResolveObject(target_directory) if dir_object.contents: raise OSError(errno.ENOTEMPTY, 'Fake Directory not empty', target_directory) try: self.RemoveObject(target_directory) - except IOError as e: - raise OSError(e.errno, e.strerror, e.filename) + except IOError as exc: + raise OSError(exc.errno, exc.strerror, exc.filename) def ListDir(self, target_directory): """Returns a sorted list of filenames in target_directory. @@ -1720,7 +1757,7 @@ def ListDir(self, target_directory): OSError: if the target is not a directory """ target_directory = self.ResolvePath(target_directory) - directory = self._ConfirmDir(target_directory) + directory = self.ConfirmDir(target_directory) return sorted(directory.contents) def __str__(self): @@ -1785,8 +1822,8 @@ def getsize(self, path): try: file_obj = self.filesystem.GetObject(path) return file_obj.st_size - except IOError as e: - raise os.error(e.errno, e.strerror) + except IOError as exc: + raise os.error(exc.errno, exc.strerror) def _istype(self, path, st_flag): """Helper function to implement isdir(), islink(), etc. @@ -1814,12 +1851,14 @@ def _istype(self, path, st_flag): return False def isabs(self, path): + """Return True if path is an absolute pathname.""" if self.filesystem.supports_drive_letter: path = self.splitdrive(path)[1] - if _is_windows: + if _IS_WINDOWS: return len(path) > 0 and path[0] in (self.sep, self.altsep) else: - return path.startswith(self.sep) or self.altsep is not None and path.startswith(self.altsep) + return path.startswith(self.sep) or self.altsep is not None and path.startswith( + self.altsep) def isdir(self, path): """Determines if path identifies a directory.""" @@ -1855,15 +1894,16 @@ def getmtime(self, path): """Returns the mtime of the file.""" try: file_obj = self.filesystem.GetObject(path) - except IOError as e: - raise OSError(errno.ENOENT, str(e)) + except IOError as exc: + raise OSError(errno.ENOENT, str(exc)) return file_obj.st_mtime def abspath(self, path): """Return the absolute version of a path.""" def getcwd(): - if sys.version_info < (3, 0) and isinstance(path, unicode): + """Return the current working directory.""" + if sys.version_info < (3, 0) and isinstance(path, unicode): # pylint: disable=undefined-variable return self.os.getcwdu() else: return self.os.getcwd() @@ -1871,9 +1911,10 @@ def getcwd(): if not self.isabs(path): path = self.join(getcwd(), path) elif (self.filesystem.supports_drive_letter and - path.startswith(self.sep) or self.altsep is not None and path.startswith(self.altsep)): + path.startswith(self.sep) or self.altsep is not None and + path.startswith(self.altsep)): cwd = getcwd() - if self.filesystem._StartsWithDriveLetter(cwd): + if self.filesystem.StartsWithDriveLetter(cwd): path = self.join(cwd[:2], path) return self.normpath(path) @@ -1892,11 +1933,11 @@ def normpath(self, path): def normcase(self, path): """Converts to lower case under windows, replaces additional path separator""" path = self.filesystem.NormalizePathSeparator(path) - if _is_windows: + if _IS_WINDOWS: path = path.lower() return path - if _is_windows: + if _IS_WINDOWS: def relpath(self, path, start=None): """ntpath.relpath() needs the cwd passed in the start argument.""" @@ -1908,6 +1949,9 @@ def relpath(self, path, start=None): realpath = abspath def expanduser(self, path): + """Return the argument with an initial component of ~ or ~user + replaced by that user's home directory. + """ return self._os_path.expanduser(path).replace(self._os_path.sep, self.sep) def ismount(self, path): @@ -1925,7 +1969,10 @@ def ismount(self, path): normed_path = self.filesystem.NormalizePath(path) if self.filesystem.supports_drive_letter: if self.filesystem.alternative_path_separator is not None: - path_seps = (self.filesystem.path_separator, self.filesystem.alternative_path_separator) + path_seps = ( + self.filesystem.path_separator, + self.filesystem.alternative_path_separator + ) else: path_seps = (self.filesystem.path_separator,) drive, rest = self.filesystem.SplitDrive(normed_path) @@ -1998,7 +2045,7 @@ def _fdopen(self, *args, **kwargs): raise TypeError('an integer is required') return FakeFileOpen(self.filesystem)(*args, **kwargs) - def _fdopen_ver2(self, file_des, mode='r', bufsize=None): + def _fdopen_ver2(self, file_des, mode='r', bufsize=None): # pylint: disable=unused-argument """Returns an open file object connected to the file descriptor file_des. Args: @@ -2019,8 +2066,8 @@ def _fdopen_ver2(self, file_des, mode='r', bufsize=None): try: return FakeFileOpen(self.filesystem).Call(file_des, mode=mode) - except IOError as e: - raise OSError(e) + except IOError as exc: + raise OSError(exc) def open(self, file_path, flags, mode=None): """Returns the file descriptor for a FakeFile. @@ -2059,8 +2106,8 @@ def close(self, file_des): OSError: bad file descriptor. TypeError: if file descriptor is not an integer. """ - fh = self.filesystem.GetOpenFile(file_des) - fh.close() + file_handle = self.filesystem.GetOpenFile(file_des) + file_handle.close() def read(self, file_des, num_bytes): """Reads number of bytes from a file descriptor, returns bytes read. @@ -2076,8 +2123,8 @@ def read(self, file_des, num_bytes): OSError: bad file descriptor. TypeError: if file descriptor is not an integer. """ - fh = self.filesystem.GetOpenFile(file_des) - return fh.read(num_bytes) + file_handle = self.filesystem.GetOpenFile(file_des) + return file_handle.read(num_bytes) def write(self, file_des, contents): """Writes string to file descriptor, returns number of bytes written. @@ -2093,9 +2140,9 @@ def write(self, file_des, contents): OSError: bad file descriptor. TypeError: if file descriptor is not an integer. """ - fh = self.filesystem.GetOpenFile(file_des) - fh.write(contents) - fh.flush() + file_handle = self.filesystem.GetOpenFile(file_des) + file_handle.write(contents) + file_handle.flush() return len(contents) @classmethod @@ -2162,7 +2209,7 @@ def chdir(self, target_directory): the target is not a directory """ target_directory = self.filesystem.ResolvePath(target_directory) - self.filesystem._ConfirmDir(target_directory) + self.filesystem.ConfirmDir(target_directory) directory = self.filesystem.GetObject(target_directory) # A full implementation would check permissions all the way up the tree. if not directory.st_mode | PERM_EXE: @@ -2178,7 +2225,7 @@ def getcwdu(self): """Return current working directory. Deprecated in Python 3.""" if sys.version_info >= (3, 0): raise AttributeError('no attribute getcwdu') - return unicode(self.filesystem.cwd) + return unicode(self.filesystem.cwd) # pylint: disable=undefined-variable def listdir(self, target_directory): """Returns a sorted list of filenames in target_directory. @@ -2200,6 +2247,11 @@ class DirEntry(): """Emulates os.DirEntry. Note that we did not enforce keyword only arguments.""" def __init__(self, fake_os): + """Initialize the dir entry with unset values. + + Args: + fake_os: the fake os module used for implementation. + """ self._fake_os = fake_os self.name = '' self.path = '' @@ -2210,25 +2262,50 @@ def __init__(self, fake_os): self._statresult_symlink = None def inode(self): + """Return the inode number of the entry.""" if self._inode is None: self.stat(follow_symlinks=False) return self._inode def is_dir(self, follow_symlinks=True): + """Return True if this entry is a directory or a symbolic link + pointing to a directory; return False if the entry is or points + to any other kind of file, or if it doesn't exist anymore. + + Args: + follow_symlinks: If False, return True only if this entry is a directory + (without following symlinks) + """ return self._isdir and (follow_symlinks or not self._islink) def is_file(self, follow_symlinks=True): + """Return True if this entry is a file or a symbolic link + pointing to a file; return False if the entry is or points + to a directory or other non-file entry, or if it doesn't exist anymore. + + Args: + follow_symlinks: If False, return True only if this entry is a file + (without following symlinks) + """ return not self._isdir and (follow_symlinks or not self._islink) def is_symlink(self): + """Return True if this entry is a symbolic link (even if broken).""" return self._islink def stat(self, follow_symlinks=True): + """Return a stat_result object for this entry. + + Args: + follow_symlinks: If False and the entry is a symlink, return the + result for the symlink, otherwise for the object is points to. + """ if follow_symlinks: if self._statresult_symlink is None: stats = self._fake_os.filesystem.ResolveObject(self.path) - if _is_windows: - # under Windows, some properties are 0 probably due to performance reasons + if _IS_WINDOWS: + # under Windows, some properties are 0 + # probably due to performance reasons stats.st_ino = 0 stats.st_dev = 0 stats.st_nlink = 0 @@ -2242,7 +2319,7 @@ def stat(self, follow_symlinks=True): if self._statresult is None: stats = self._fake_os.filesystem.LResolveObject(self.path) self._inode = stats.st_ino - if _is_windows: + if _IS_WINDOWS: stats.st_ino = 0 stats.st_dev = 0 stats.st_nlink = 0 @@ -2254,7 +2331,8 @@ def stat(self, follow_symlinks=True): return self._statresult def scandir(self, path=''): - """Return an iterator of DirEntry objects corresponding to the entries in the directory given by path. + """Return an iterator of DirEntry objects corresponding to the entries + in the directory given by path. Args: path: path to the target directory within the fake filesystem @@ -2266,7 +2344,7 @@ def scandir(self, path=''): OSError: if the target is not a directory """ path = self.filesystem.ResolvePath(path) - fake_dir = self.filesystem._ConfirmDir(path) + fake_dir = self.filesystem.ConfirmDir(path) for entry in fake_dir.contents: dir_entry = self.DirEntry(self) dir_entry.name = entry @@ -2321,10 +2399,10 @@ def walk(self, top, topdown=True, onerror=None, followlinks=False): return try: top_contents = self._ClassifyDirectoryContents(top) - except OSError as e: + except OSError as exc: top_contents = None if onerror is not None: - onerror(e) + onerror(exc) if top_contents is not None: if topdown: @@ -2334,7 +2412,8 @@ def walk(self, top, topdown=True, onerror=None, followlinks=False): if not followlinks and self.path.islink(directory): continue for contents in self.walk(self.path.join(top, directory), - topdown=topdown, onerror=onerror, followlinks=followlinks): + topdown=topdown, onerror=onerror, + followlinks=followlinks): yield contents if not topdown: @@ -2458,7 +2537,7 @@ def rmdir(self, target_directory): def removedirs(self, target_directory): """Remove a leaf Fake directory and all empty intermediate ones.""" target_directory = self.filesystem.NormalizePath(target_directory) - directory = self.filesystem._ConfirmDir(target_directory) + directory = self.filesystem.ConfirmDir(target_directory) if directory.contents: raise OSError(errno.ENOTEMPTY, 'Fake Directory not empty', self.path.basename(target_directory)) @@ -2468,7 +2547,7 @@ def removedirs(self, target_directory): if not tail: head, tail = self.path.split(head) while head and tail: - head_dir = self.filesystem._ConfirmDir(head) + head_dir = self.filesystem.ConfirmDir(head) if head_dir.contents: break self.rmdir(head) @@ -2525,12 +2604,12 @@ def access(self, path, mode, follow_symlinks=None): if follow_symlinks is not None and sys.version_info < (3, 3): raise TypeError("access() got an unexpected keyword argument 'follow_symlinks'") try: - st = self.stat(path, follow_symlinks) + stat_result = self.stat(path, follow_symlinks) except OSError as os_error: if os_error.errno == errno.ENOENT: return False raise - return (mode & ((st.st_mode >> 6) & 7)) == mode + return (mode & ((stat_result.st_mode >> 6) & 7)) == mode def chmod(self, path, mode, follow_symlinks=None): """Change the permissions of a file as encoded in integer mode. @@ -2547,7 +2626,7 @@ def chmod(self, path, mode, follow_symlinks=None): raise TypeError("chmod() got an unexpected keyword argument 'follow_symlinks'") self.filesystem.ChangeMode(path, mode, follow_symlinks) - if not _is_windows: + if not _IS_WINDOWS: def lchmod(self, path, mode): """Change the permissions of a file as encoded in integer mode. If the file is a link, the permissions of the link are changed. @@ -2604,7 +2683,7 @@ def chown(self, path, uid, gid, follow_symlinks=None): 'No such file or directory in fake filesystem', path) if not ((isinstance(uid, int) or uid is None) and - (isinstance(gid, int) or gid is None)): + (isinstance(gid, int) or gid is None)): raise TypeError("An integer is required") if uid != -1: file_object.st_uid = uid @@ -2807,12 +2886,13 @@ def __init__(self, file_object, file_path, update=False, read=False, append=Fals # override, don't modify FakeFile.name, as FakeFilesystem expects # it to be the file name only, no directories. self.name = file_object.opened_as + self.filedes = None def __enter__(self): """To support usage of this fake file with the 'with' statement.""" return self - def __exit__(self, type, value, traceback): # pylint: disable-msg=W0622 + def __exit__(self, type, value, traceback): # pylint: disable=redefined-builtin """To support usage of this fake file with the 'with' statement.""" self.close() @@ -3106,9 +3186,8 @@ def Call(self, file_, mode='r', buffering=-1, encoding=None, def _RunDoctest(): - # pylint: disable-msg=C6204 import doctest - from pyfakefs import fake_filesystem # pylint: disable-msg=W0406 + from pyfakefs import fake_filesystem # pylint: disable=import-self return doctest.testmod(fake_filesystem) diff --git a/pyfakefs/fake_filesystem_glob.py b/pyfakefs/fake_filesystem_glob.py index 16d61dff..436a0e89 100755 --- a/pyfakefs/fake_filesystem_glob.py +++ b/pyfakefs/fake_filesystem_glob.py @@ -17,8 +17,8 @@ Includes: FakeGlob: Uses a FakeFilesystem to provide a fake replacement for the glob module. -Note: Code is taken form Python 3.5 and slightly adapted to work with older versions - and use the fake os and os.path modules +Note: Code is taken form Python 3.5 and slightly adapted to work with older + versions and use the fake os and os.path modules Usage: >>> from pyfakefs import fake_filesystem @@ -68,10 +68,11 @@ def glob(self, pathname, recursive=None): Returns: List of strings matching the glob pattern. """ - return list(self.iglob(pathname, recursive=_recursive_from_arg(recursive))) + return list( + self.iglob(pathname, recursive=_recursive_from_arg(recursive))) def iglob(self, pathname, recursive=None): - """Return an iterator which yields the paths matching a pathname pattern. + """Return an iterator yielding the paths matching a pathname pattern. The pattern may contain shell-style wildcards a la fnmatch. @@ -81,11 +82,11 @@ def iglob(self, pathname, recursive=None): zero or more directories and subdirectories. (>= Python 3.5 only) """ recursive = _recursive_from_arg(recursive) - it = self._iglob(pathname, recursive) + itr = self._iglob(pathname, recursive) if recursive and _isrecursive(pathname): - s = next(it) # skip empty string - assert not s - return it + string = next(itr) # skip empty string + assert not string + return itr def _iglob(self, pathname, recursive): dirname, basename = self._path_module.split(pathname) @@ -106,8 +107,9 @@ def _iglob(self, pathname, recursive): for name in self.glob1(dirname, basename): yield name return - # `self._path_module.split()` returns the argument itself as a dirname if it is a - # drive or UNC path. Prevent an infinite recursion if a drive or UNC path + # `self._path_module.split()` returns the argument itself as a dirname + # if it is a drive or UNC path. + # Prevent an infinite recursion if a drive or UNC path # contains magic characters (i.e. r'\\?\C:'). if dirname != pathname and self.has_magic(dirname): dirs = self._iglob(dirname, recursive) @@ -125,15 +127,17 @@ def _iglob(self, pathname, recursive): yield self._path_module.join(dirname, name) # These 2 helper functions non-recursively glob inside a literal directory. - # They return a list of basenames. `glob1` accepts a pattern while `glob0` + # They return a list of basenames. `_glob1` accepts a pattern while `_glob0` # takes a literal basename (so it only has to check for its existence). def glob1(self, dirname, pattern): if not dirname: + # pylint: disable=undefined-variable if sys.version_info >= (3,) and isinstance(pattern, bytes): dirname = bytes(self._os_module.curdir, 'ASCII') elif sys.version_info < (3,) and isinstance(pattern, unicode): - dirname = unicode(self._os_module.curdir, - sys.getfilesystemencoding() or sys.getdefaultencoding()) + dirname = unicode( + self._os_module.curdir, + sys.getfilesystemencoding() or sys.getdefaultencoding()) else: dirname = self._os_module.curdir @@ -147,17 +151,19 @@ def glob1(self, dirname, pattern): def glob0(self, dirname, basename): if not basename: - # `self._path_module.split()` returns an empty basename for paths ending with a - # directory separator. 'q*x/' should match only directories. + # `self._path_module.split()` returns an empty basename + # for paths ending with a directory separator. + # 'q*x/' should match only directories. if self._path_module.isdir(dirname): return [basename] else: - if self._path_module.lexists(self._path_module.join(dirname, basename)): + if self._path_module.lexists( + self._path_module.join(dirname, basename)): return [basename] return [] - # This helper function recursively yields relative pathnames inside a literal - # directory. + # This helper function recursively yields relative pathnames + # inside a literal directory. def glob2(self, dirname, pattern): assert _isrecursive(pattern) yield pattern[:0] @@ -167,11 +173,13 @@ def glob2(self, dirname, pattern): # Recursively yields relative pathnames inside a literal directory. def _rlistdir(self, dirname): if not dirname: + # pylint: disable=undefined-variable if sys.version_info >= (3,) and isinstance(dirname, bytes): dirname = bytes(self._os_module.curdir, 'ASCII') elif sys.version_info < (3,) and isinstance(dirname, unicode): - dirname = unicode(self._os_module.curdir, - sys.getfilesystemencoding() or sys.getdefaultencoding()) + dirname = unicode( + self._os_module.curdir, + sys.getfilesystemencoding() or sys.getdefaultencoding()) else: dirname = self._os_module.curdir @@ -179,21 +187,22 @@ def _rlistdir(self, dirname): names = self._os_module.listdir(dirname) except self._os_module.error: return - for x in names: - if not _ishidden(x): - yield x - path = self._path_module.join(dirname, x) if dirname else x - for y in self._rlistdir(path): - yield self._path_module.join(x, y) + for name in names: + if not _ishidden(name): + yield name + path = self._path_module.join(dirname, + name) if dirname else name + for dir_name in self._rlistdir(path): + yield self._path_module.join(name, dir_name) magic_check = re.compile('([*?[])') magic_check_bytes = re.compile(b'([*?[])') - def has_magic(self, s): - if isinstance(s, bytes): - match = self.magic_check_bytes.search(s) + def has_magic(self, string): + if isinstance(string, bytes): + match = self.magic_check_bytes.search(string) else: - match = self.magic_check.search(s) + match = self.magic_check.search(string) return match is not None def escape(self, pathname): @@ -234,7 +243,7 @@ def _recursive_from_arg(recursive): def _RunDoctest(): - # pylint: disable-msg=C6111,C6204,W0406 + # pylint: disable=import-self import doctest from pyfakefs import fake_filesystem_glob return doctest.testmod(fake_filesystem_glob) diff --git a/pyfakefs/fake_filesystem_shutil.py b/pyfakefs/fake_filesystem_shutil.py index 8a4afc80..1bce5ec9 100755 --- a/pyfakefs/fake_filesystem_shutil.py +++ b/pyfakefs/fake_filesystem_shutil.py @@ -13,8 +13,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -# pylint: disable-msg=W0612,W0613,C6409 """A fake shutil module implementation that uses fake_filesystem for unit tests. @@ -43,9 +41,10 @@ import errno import os import shutil -import stat import sys +import stat + __pychecker__ = 'no-reimportself' _PERM_WRITE = 0o200 # Write permission bit. @@ -90,11 +89,8 @@ def rmtree(self, path, ignore_errors=False, onerror=None): be caught. """ if ignore_errors: - def onerror(*args): + def onerror(*args): # pylint: disable=unused-argument,function-redefined pass - elif onerror is None: - def onerror(*args): - raise try: if not self.filesystem.Exists(path): raise IOError("The specified path does not exist") @@ -102,12 +98,16 @@ def onerror(*args): # symlinks to directories are forbidden. raise OSError("Cannot call rmtree on a symbolic link") except Exception: + if onerror is None: + raise onerror(os.path.islink, path, sys.exc_info()) # can't continue even if onerror hook returns return try: self.filesystem.RemoveObject(path) except (IOError, OSError): + if onerror is None: + raise onerror(FakeShutilModule.rmtree, path, sys.exc_info()) def copy(self, src, dst): @@ -202,8 +202,8 @@ def _copytree(self, src, dst, copy_function, symlinks): self.filesystem.CreateDirectory(dst) try: directory = self.filesystem.GetObject(src) - except IOError as e: - raise OSError(e.errno, e.message) + except IOError as exception: + raise OSError(exception.errno, exception.message) if not stat.S_ISDIR(directory.st_mode): raise OSError(errno.ENOTDIR, 'Fake os module: %r not a directory' % src) @@ -310,7 +310,7 @@ def __getattr__(self, name): def _RunDoctest(): - # pylint: disable-msg=C6111,C6204,W0406 + # pylint: disable=import-self import doctest from pyfakefs import fake_filesystem_shutil return doctest.testmod(fake_filesystem_shutil) diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index f5800c2b..34cc05f7 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -40,28 +40,29 @@ """ import sys - -if sys.version_info < (2, 7): - import unittest2 as unittest -else: - import unittest import doctest import inspect + +import mox3.stubout + from pyfakefs import fake_filesystem from pyfakefs import fake_filesystem_shutil from pyfakefs import fake_tempfile if sys.version_info >= (3, 4): from pyfakefs import fake_pathlib +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + if sys.version_info < (3,): - import __builtin__ as builtins + import __builtin__ as builtins # pylint: disable=import-error else: import builtins -import mox3.stubout - -def load_doctests(loader, tests, ignore, module): +def load_doctests(loader, tests, ignore, module): # pylint: disable=unused-argument """Load the doctest tests for the specified module into unittest.""" _patcher = Patcher() globs = _patcher.replaceGlobs(vars(module)) @@ -73,24 +74,32 @@ def load_doctests(loader, tests, ignore, module): class TestCase(unittest.TestCase): + """Test case class that automatically replaces file-system related + modules by fake implementations. + """ + def __init__(self, methodName='runTest', additional_skip_names=None, patch_path=True): - """Creates the test class instance and the stubber used to stub out file system related modules. + """Creates the test class instance and the stubber used to stub out + file system related modules. - Args: + Args: methodName: the name of the test method (as in unittest.TestCase) - additional_skip_names: names of modules inside of which no module replacement shall be done - (additionally to the hard-coded list: 'os', 'glob', 'path', 'tempfile', 'io') - patch_path: if False, modules named 'path' will not be patched with the fake 'os.path' module. - Set this to False when you need to import some other module named 'path', for example, + additional_skip_names: names of modules inside of which no module replacement + shall be done + (additionally to the hard-coded list: 'os', 'glob', 'path', 'tempfile', 'io') + patch_path: if False, modules named 'path' will not be patched with the + fake 'os.path' module. Set this to False when you need to import + some other module named 'path', for example, `from my_module import path` - Irrespective of patch_path, module 'os.path' is still correctly faked if imported the usual way - using `import os` or `import os.path`. + Irrespective of patch_path, module 'os.path' is still correctly faked + if imported the usual way using `import os` or `import os.path`. Example usage in a derived test class: class MyTestCase(fake_filesystem_unittest.TestCase): def __init__(self, methodName='runTest'): - super(MyTestCase, self).__init__(methodName=methodName, additional_skip_names=['posixpath']) + super(MyTestCase, self).__init__( + methodName=methodName, additional_skip_names=['posixpath']) """ super(TestCase, self).__init__(methodName) self._stubber = Patcher(additional_skip_names=additional_skip_names, @@ -155,13 +164,13 @@ def __init__(self, additional_skip_names=None, patch_path=True): self._skipNames.discard('path') # Attributes set by _findModules() - self._osModules = None - self._pathModules = None + self._os_modules = None + self._path_modules = None if self.HAS_PATHLIB: - self._pathlibModules = None - self._shutilModules = None - self._tempfileModules = None - self._ioModules = None + self._pathlib_modules = None + self._shutil_modules = None + self._tempfile_modules = None + self._io_modules = None self._findModules() assert None not in vars(self).values(), \ "_findModules() missed the initialization of an instance variable" @@ -189,30 +198,30 @@ def _findModules(self): Later, `setUp()` will stub these with the fake file system modules. """ - self._osModules = set() - self._pathModules = set() + self._os_modules = set() + self._path_modules = set() if self.HAS_PATHLIB: - self._pathlibModules = set() - self._shutilModules = set() - self._tempfileModules = set() - self._ioModules = set() + self._pathlib_modules = set() + self._shutil_modules = set() + self._tempfile_modules = set() + self._io_modules = set() for name, module in set(sys.modules.items()): if (module in self.SKIPMODULES or (not inspect.ismodule(module)) or - name.split('.')[0] in self._skipNames): + name.split('.')[0] in self._skipNames): continue if 'os' in module.__dict__: - self._osModules.add(module) + self._os_modules.add(module) if self._patchPath and 'path' in module.__dict__: - self._pathModules.add(module) + self._path_modules.add(module) if self.HAS_PATHLIB and 'pathlib' in module.__dict__: - self._pathlibModules.add(module) + self._pathlib_modules.add(module) if 'shutil' in module.__dict__: - self._shutilModules.add(module) + self._shutil_modules.add(module) if 'tempfile' in module.__dict__: - self._tempfileModules.add(module) + self._tempfile_modules.add(module) if 'io' in module.__dict__: - self._ioModules.add(module) + self._io_modules.add(module) def _refresh(self): """Renew the fake file system and set the _isStale flag to `False`.""" @@ -246,18 +255,18 @@ def setUp(self, doctester=None): self._stubs.SmartSet(builtins, 'file', self.fake_open) self._stubs.SmartSet(builtins, 'open', self.fake_open) - for module in self._osModules: + for module in self._os_modules: self._stubs.SmartSet(module, 'os', self.fake_os) - for module in self._pathModules: + for module in self._path_modules: self._stubs.SmartSet(module, 'path', self.fake_path) if self.HAS_PATHLIB: - for module in self._pathlibModules: + for module in self._pathlib_modules: self._stubs.SmartSet(module, 'pathlib', self.fake_pathlib) - for module in self._shutilModules: + for module in self._shutil_modules: self._stubs.SmartSet(module, 'shutil', self.fake_shutil) - for module in self._tempfileModules: + for module in self._tempfile_modules: self._stubs.SmartSet(module, 'tempfile', self.fake_tempfile_) - for module in self._ioModules: + for module in self._io_modules: self._stubs.SmartSet(module, 'io', self.fake_io) def replaceGlobs(self, globs_): diff --git a/pyfakefs/fake_tempfile.py b/pyfakefs/fake_tempfile.py index 8a29b608..b07b5c1f 100644 --- a/pyfakefs/fake_tempfile.py +++ b/pyfakefs/fake_tempfile.py @@ -17,21 +17,21 @@ Fake implementation of the python2.4.1 tempfile built-in module that works with a FakeFilesystem object. """ -# pylint: disable-all import errno import logging import os -import stat import tempfile import warnings +import stat + from pyfakefs import fake_filesystem try: - import StringIO as io # pylint: disable-msg=C6204 + import StringIO as io except ImportError: - import io # pylint: disable-msg=C6204 + import io class FakeTempfileModule(object): @@ -52,8 +52,7 @@ def __init__(self, filesystem): self._temp_prefix = 'tmp' self._mktemp_retvals = [] - # pylint: disable-msg=W0622 - def _TempFilename(self, suffix='', prefix=None, dir=None): + def _TempFilename(self, suffix='', prefix=None, directory=None): """Create a temporary filename that does not exist. This is a re-implementation of how tempfile creates random filenames, @@ -65,43 +64,52 @@ def _TempFilename(self, suffix='', prefix=None, dir=None): Args: suffix: filename suffix prefix: filename prefix - dir: dir to put filename in + directory: dir to put filename in Returns: string, temp filename that does not exist """ - if dir is None: - dir = self._filesystem.JoinPaths(self._filesystem.root.name, 'tmp') + if directory is None: + directory = self._filesystem.JoinPaths( + self._filesystem.root.name, 'tmp') filename = None if prefix is None: prefix = self._temp_prefix while not filename or self._filesystem.Exists(filename): - # pylint: disable-msg=W0212 - filename = self._filesystem.JoinPaths(dir, '%s%s%s' % ( + # pylint: disable=protected-access + filename = self._filesystem.JoinPaths(directory, '%s%s%s' % ( prefix, next(self._tempfile._RandomNameSequence()), suffix)) return filename - # pylint: disable-msg=W0622,W0613 - def TemporaryFile(self, mode='w+b', bufsize=-1, + # pylint: disable=redefined-builtin,unused-argument,too-many-arguments + @staticmethod + def TemporaryFile(mode='w+b', bufsize=-1, suffix='', prefix=None, dir=None): """Return a file-like object deleted on close(). - Python 2.4.1 tempfile.TemporaryFile.__doc__ = - >Return a file (or file-like) object that can be used as a temporary - >storage area. The file is created using mkstemp. It will be destroyed as - >soon as it is closed (including an implicit close when the object is - >garbage collected). Under Unix, the directory entry for the file is - >removed immediately after the file is created. Other platforms do not - >support this; your code should not rely on a temporary file created using - >this function having or not having a visible name in the file system. + Python 3.5 tempfile.TemporaryFile.__doc__ = + >Return a file-like object that can be used as a temporary storage area. + >The file is created securely, using the same rules as mkstemp(). + >It will be destroyed as soon as it is closed (including an implicit + >close when the object is garbage collected). Under Unix, the directory + >entry for the file is either not created at all or is removed + >immediately after the file is created. Other platforms do not support + >this; your code should not rely on a temporary file created using this + >function having or not having a visible name in the file system. + > + >The resulting object can be used as a context manager (see Examples). + >On completion of the context or destruction of the file object the + >temporary file will be removed from the filesystem. > - >The mode parameter defaults to 'w+b' so that the file created can be read - >and written without being closed. Binary mode is used so that it behaves - >consistently on all platforms without regard for the data that is stored. - >bufsize defaults to -1, meaning that the operating system default is used. + >The mode parameter defaults to 'w+b' so that the file created can be + >read and written without being closed. Binary mode is used so that it + >behaves consistently on all platforms without regard for the data that + >is stored. buffering, encoding and newline are interpreted as for + >open(). > - >The dir, prefix and suffix parameters are passed to mkstemp() + >The dir, prefix and suffix parameters have the same meaning and + >defaults as with mkstemp(). Args: mode: optional string, see above @@ -112,14 +120,13 @@ def TemporaryFile(self, mode='w+b', bufsize=-1, Returns: a file-like object. """ - # pylint: disable-msg=C6002 # TODO: prefix, suffix, bufsize, dir, mode unused? # cannot be cStringIO due to .name requirement below retval = io.StringIO() retval.name = '' # as seen on 2.4.3 return retval - # pylint: disable-msg=W0622,W0613 + # pylint: disable=redefined-builtin,unused-argument def NamedTemporaryFile(self, mode='w+b', bufsize=-1, suffix='', prefix=None, dir=None, delete=True): """Return a file-like object with name that is deleted on close(). @@ -139,7 +146,6 @@ def NamedTemporaryFile(self, mode='w+b', bufsize=-1, Returns: a file-like object including obj.name """ - # pylint: disable-msg=C6002 # TODO: bufsiz unused? temp = self.mkstemp(suffix=suffix, prefix=prefix, dir=dir) filename = temp[1] @@ -147,11 +153,11 @@ def NamedTemporaryFile(self, mode='w+b', bufsize=-1, self._filesystem, delete_on_close=delete) obj = mock_open(filename, mode) obj.name = filename - # remove file wrapper created by mkstemp to avoid double open file entries + # remove file wrapper created by mkstemp to avoid double + # open file entries self._filesystem.CloseOpenFile(temp[0]) return obj - # pylint: disable-msg=C6409 def mkstemp(self, suffix='', prefix=None, dir=None, text=False): """Create temp file, returning a 2-tuple: (9999, filename). @@ -189,18 +195,17 @@ def mkstemp(self, suffix='', prefix=None, dir=None, text=False): Raises: OSError: when dir= is specified but does not exist """ - # pylint: disable-msg=C6002 # TODO: optional boolean text is unused? # default dir affected by "global" filename = self._TempEntryname(suffix, prefix, dir) - fh = self._filesystem.CreateFile(filename, st_mode=stat.S_IFREG | 0o600) - fh.opened_as = filename - fakefile = fake_filesystem.FakeFileWrapper(fh, filename) - fd = self._filesystem.AddOpenFile(fakefile) + file_handle = \ + self._filesystem.CreateFile(filename, st_mode=stat.S_IFREG | 0o600) + file_handle.opened_as = filename + fakefile = fake_filesystem.FakeFileWrapper(file_handle, filename) + file_des = self._filesystem.AddOpenFile(fakefile) self._mktemp_retvals.append(filename) - return (fd, filename) + return file_des, filename - # pylint: disable-msg=C6409 def mkdtemp(self, suffix='', prefix=None, dir=None): """Create temp directory, returns string, absolute pathname. @@ -246,7 +251,8 @@ def _TempEntryname(self, suffix, prefix, dir): entryname = None while not entryname or self._filesystem.Exists(entryname): - entryname = self._TempFilename(suffix=suffix, prefix=prefix, dir=dir) + entryname = self._TempFilename( + suffix=suffix, prefix=prefix, directory=dir) if not call_mkdir: # This is simplistic. A bad input of suffix=/f will cause tempfile # to blow up, but this mock won't. But that's already a broken @@ -256,25 +262,24 @@ def _TempEntryname(self, suffix, prefix, dir): self._filesystem.GetObject(parent_dir) except IOError as err: assert 'No such file or directory' in str(err) - # python -c 'import tempfile; tempfile.mkstemp(dir="/no/such/dr")' - # OSError: [Errno 2] No such file or directory: '/no/such/dr/tmpFBuqjO' + # python -c 'import tempfile; + # tempfile.mkstemp(dir="/no/such/dr")' + # OSError: [Errno 2] No such file or directory: + # '/no/such/dr/tmpFBuqjO' raise OSError( errno.ENOENT, 'No such directory in mock filesystem', parent_dir) return entryname - # pylint: disable-msg=C6409 def gettempdir(self): """Get default temp dir. Sets default if unset.""" if self.tempdir: return self.tempdir - # pylint: disable-msg=C6002 # TODO: environment variables TMPDIR TEMP TMP, or other dirs? self.tempdir = '/tmp' return self.tempdir - # pylint: disable-msg=C6409 def gettempprefix(self): """Get temp filename prefix. @@ -285,7 +290,6 @@ def gettempprefix(self): """ return self._temp_prefix - # pylint: disable-msg=C6409 def mktemp(self, suffix=''): """mktemp is deprecated in 2.4.1, and is thus unimplemented.""" raise NotImplementedError @@ -338,15 +342,12 @@ def TemporaryDirectory(self, suffix='', prefix='tmp', dir=None): """ class FakeTemporaryDirectory(object): - def __init__(self, filesystem, tempfile, suffix=None, prefix=None, dir=None): + def __init__(self, filesystem, tempfile, + suffix=None, prefix=None, dir=None): self.closed = False self.filesystem = filesystem self.name = tempfile.mkdtemp(suffix, prefix, dir) - def cleanup(self, _warn=False): - self.filesystem.RemoveObject(name) - warnings.warn(warn_message, ResourceWarning) - def __repr__(self): return "<{} {!r}>".format(self.__class__.__name__, self.name) @@ -361,11 +362,12 @@ def cleanup(self, warn=False): self.filesystem.RemoveObject(self.name) self.closed = True if warn: - warnings.warn("Implicitly cleaning up {!r}".format(self), - ResourceWarning) + warnings.warn("Implicitly cleaning up {!r}" + .format(self), ResourceWarning) def __del__(self): # Issue a ResourceWarning if implicit cleanup needed self.cleanup(warn=True) - return FakeTemporaryDirectory(self._filesystem, self, suffix, prefix, dir) + return FakeTemporaryDirectory( + self._filesystem, self, suffix, prefix, dir) From ebc0c19ff64e29b633546cf774a884d3fbf1848c Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Tue, 8 Nov 2016 20:09:43 +0100 Subject: [PATCH 05/10] Added support for Python 3.6 - added support for context manager in scandir iterator (used in pathlib and glob) - fixed incorrect usage of random generator in fake_tempfile - added Python 3.6 in Travis CI --- .travis.yml | 1 + CHANGES.md | 6 +- fake_filesystem_test.py | 5 +- pyfakefs/fake_filesystem.py | 333 ++++++++++++++++++------------- pyfakefs/fake_filesystem_glob.py | 2 +- pyfakefs/fake_pathlib.py | 3 + pyfakefs/fake_tempfile.py | 6 +- 7 files changed, 213 insertions(+), 143 deletions(-) diff --git a/.travis.yml b/.travis.yml index e319918b..848cd544 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6-dev" install: - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install importlib unittest2; fi diff --git a/CHANGES.md b/CHANGES.md index bd4006de..2576da4b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ The release versions are PyPi releases. #### New Features * `io.open`, `os.open`: support for `encoding` argument - * `os.makedirs`: support for `exist_ok` argument + * `os.makedirs`: support for `exist_ok` argument (Python >= 3.2) * support for fake `io.open()` * support for mount points * support for hard links @@ -25,7 +25,7 @@ The release versions are PyPi releases. * Windows support: * support for alternative path separator (Windows) * support for case-insensitive filesystems - * supprt for drive letters and UNC paths + * support for drive letters and UNC paths * support for filesystem size * `shutil.rmtree`: support for `ignore_errors` and `onerror` arguments * support for `os.fsync()` and `os.fdatasync()` @@ -40,7 +40,7 @@ The release versions are PyPi releases. ## Version 2.7 -### Infrastructure +#### Infrastructure * moved repository from GoogleCode to GitHub, merging 3 projects * added continous integration testing with Travis CI * added usage documentation in project wiki diff --git a/fake_filesystem_test.py b/fake_filesystem_test.py index e222df9f..62e60629 100755 --- a/fake_filesystem_test.py +++ b/fake_filesystem_test.py @@ -2460,7 +2460,10 @@ def testRelpath(self): path_foo = '/path/to/foo' path_bar = '/path/to/bar' path_other = '/some/where/else' - self.assertRaises(ValueError, self.path.relpath, None) + if sys.version_info >= (3, 6): + self.assertRaises(TypeError, self.path.relpath, None) + else: + self.assertRaises(ValueError, self.path.relpath, None) self.assertRaises(ValueError, self.path.relpath, '') if sys.version_info < (2, 7): # The real Python 2.6 os.path.relpath('/path/to/foo') actually does diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 008ea0f3..c6e62431 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -102,9 +102,8 @@ import stat - if sys.version_info < (3, 0): - import cStringIO # pylint: disable=import-error + import cStringIO # pylint: disable=import-error __pychecker__ = 'no-reimportself' @@ -134,7 +133,6 @@ 'let FakeOsModule instantiate it. See the ' 'FakeOsModule docstring for details.') - _IS_WINDOWS = sys.platform.startswith('win') _IS_CYGWIN = sys.platform == 'cygwin' # Python 3.2 supports links in Windows @@ -149,6 +147,7 @@ class FakeLargeFileIoException(Exception): """Exception thrown on unsupported operations for fake large files. Fake large files have a size with no real content. """ + def __init__(self, file_path): super(FakeLargeFileIoException, self).__init__( 'Read and write operations not supported for ' @@ -1682,6 +1681,61 @@ def MakeDirectories(self, dir_name, mode=PERM_DEF, exist_ok=False): not isinstance(self.ResolveObject(dir_name), FakeDirectory)): raise + def _IsType(self, path, st_flag): + """Helper function to implement isdir(), islink(), etc. + + See the stat(2) man page for valid stat.S_I* flag values + + Args: + path: path to file to stat and test + st_flag: the stat.S_I* flag checked for the file's st_mode + + Returns: + boolean (the st_flag is set in path's st_mode) + + Raises: + TypeError: if path is None + """ + if path is None: + raise TypeError + try: + obj = self.ResolveObject(path) + if obj: + return stat.S_IFMT(obj.st_mode) == st_flag + except IOError: + return False + return False + + def IsDir(self, path): + """Determines if path identifies a directory.""" + return self._IsType(path, stat.S_IFDIR) + + def IsFile(self, path): + """Determines if path identifies a regular file.""" + return self._IsType(path, stat.S_IFREG) + + def IsLink(self, path): + """Determines if path identifies a symbolic link. + + Args: + path: path to filesystem object. + + Returns: + boolean (the st_flag is set in path's st_mode) + + Raises: + TypeError: if path is None + """ + if path is None: + raise TypeError + try: + link_obj = self.LResolveObject(path) + return stat.S_IFMT(link_obj.st_mode) == stat.S_IFLNK + except IOError: + return False + except KeyError: + return False + def ConfirmDir(self, target_directory): """Tests that the target is actually a directory, raising OSError if not. @@ -1760,6 +1814,142 @@ def ListDir(self, target_directory): directory = self.ConfirmDir(target_directory) return sorted(directory.contents) + if sys.version_info >= (3, 5): + class DirEntry(): + """Emulates os.DirEntry. Note that we did not enforce keyword only arguments.""" + + def __init__(self, filesystem): + """Initialize the dir entry with unset values. + + Args: + filesystem: the fake filesystem used for implementation. + """ + self._filesystem = filesystem + self.name = '' + self.path = '' + self._inode = None + self._islink = False + self._isdir = False + self._statresult = None + self._statresult_symlink = None + + def inode(self): + """Return the inode number of the entry.""" + if self._inode is None: + self.stat(follow_symlinks=False) + return self._inode + + def is_dir(self, follow_symlinks=True): + """Return True if this entry is a directory or a symbolic link + pointing to a directory; return False if the entry is or points + to any other kind of file, or if it doesn't exist anymore. + + Args: + follow_symlinks: If False, return True only if this entry is a directory + (without following symlinks) + """ + return self._isdir and (follow_symlinks or not self._islink) + + def is_file(self, follow_symlinks=True): + """Return True if this entry is a file or a symbolic link + pointing to a file; return False if the entry is or points + to a directory or other non-file entry, or if it doesn't exist anymore. + + Args: + follow_symlinks: If False, return True only if this entry is a file + (without following symlinks) + """ + return not self._isdir and (follow_symlinks or not self._islink) + + def is_symlink(self): + """Return True if this entry is a symbolic link (even if broken).""" + return self._islink + + def stat(self, follow_symlinks=True): + """Return a stat_result object for this entry. + + Args: + follow_symlinks: If False and the entry is a symlink, return the + result for the symlink, otherwise for the object is points to. + """ + if follow_symlinks: + if self._statresult_symlink is None: + stats = self._filesystem.ResolveObject(self.path) + if _IS_WINDOWS: + # under Windows, some properties are 0 + # probably due to performance reasons + stats.st_ino = 0 + stats.st_dev = 0 + stats.st_nlink = 0 + self._statresult_symlink = os.stat_result( + (stats.st_mode, stats.st_ino, stats.st_dev, + stats.st_nlink, stats.st_uid, stats.st_gid, + stats.st_size, stats.st_atime, + stats.st_mtime, stats.st_ctime)) + return self._statresult_symlink + + if self._statresult is None: + stats = self._filesystem.LResolveObject(self.path) + self._inode = stats.st_ino + if _IS_WINDOWS: + stats.st_ino = 0 + stats.st_dev = 0 + stats.st_nlink = 0 + self._statresult = os.stat_result( + (stats.st_mode, stats.st_ino, stats.st_dev, + stats.st_nlink, stats.st_uid, stats.st_gid, + stats.st_size, stats.st_atime, + stats.st_mtime, stats.st_ctime)) + return self._statresult + + class ScanDirIter: + def __init__(self, filesystem, path): + self.filesystem = filesystem + self.path = self.filesystem.ResolvePath(path) + contents = {} + try: + contents = self.filesystem.ConfirmDir(path).contents + except OSError: + pass + self.contents_iter = iter(contents) + + def __iter__(self): + return self + + def __next__(self): + entry = self.contents_iter.__next__() + dir_entry = self.filesystem.DirEntry(self.filesystem) + dir_entry.name = entry + dir_entry.path = self.filesystem.JoinPaths(self.path, dir_entry.name) + dir_entry._isdir = self.filesystem.IsDir(dir_entry.path) + dir_entry._islink = self.filesystem.IsLink(dir_entry.path) + return dir_entry + + if sys.version_info >= (3, 6): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): + pass + + def ScanDir(self, path=''): + """Return an iterator of DirEntry objects corresponding to the entries + in the directory given by path. + + Args: + path: path to the target directory within the fake filesystem + + Returns: + an iterator to an unsorted list of os.DirEntry objects for each entry in path + + Raises: + OSError: if the target is not a directory + """ + return self.ScanDirIter(self, path) + def __str__(self): return str(self.root) @@ -1825,31 +2015,6 @@ def getsize(self, path): except IOError as exc: raise os.error(exc.errno, exc.strerror) - def _istype(self, path, st_flag): - """Helper function to implement isdir(), islink(), etc. - - See the stat(2) man page for valid stat.S_I* flag values - - Args: - path: path to file to stat and test - st_flag: the stat.S_I* flag checked for the file's st_mode - - Returns: - boolean (the st_flag is set in path's st_mode) - - Raises: - TypeError: if path is None - """ - if path is None: - raise TypeError - try: - obj = self.filesystem.ResolveObject(path) - if obj: - return stat.S_IFMT(obj.st_mode) == st_flag - except IOError: - return False - return False - def isabs(self, path): """Return True if path is an absolute pathname.""" if self.filesystem.supports_drive_letter: @@ -1862,11 +2027,11 @@ def isabs(self, path): def isdir(self, path): """Determines if path identifies a directory.""" - return self._istype(path, stat.S_IFDIR) + return self.filesystem.IsDir(path) def isfile(self, path): """Determines if path identifies a regular file.""" - return self._istype(path, stat.S_IFREG) + return self.filesystem.IsFile(path) def islink(self, path): """Determines if path identifies a symbolic link. @@ -1880,15 +2045,7 @@ def islink(self, path): Raises: TypeError: if path is None """ - if path is None: - raise TypeError - try: - link_obj = self.filesystem.LResolveObject(path) - return stat.S_IFMT(link_obj.st_mode) == stat.S_IFLNK - except IOError: - return False - except KeyError: - return False + return self.filesystem.IsLink(path) def getmtime(self, path): """Returns the mtime of the file.""" @@ -2243,93 +2400,6 @@ def listdir(self, target_directory): return self.filesystem.ListDir(target_directory) if sys.version_info >= (3, 5): - class DirEntry(): - """Emulates os.DirEntry. Note that we did not enforce keyword only arguments.""" - - def __init__(self, fake_os): - """Initialize the dir entry with unset values. - - Args: - fake_os: the fake os module used for implementation. - """ - self._fake_os = fake_os - self.name = '' - self.path = '' - self._inode = None - self._islink = False - self._isdir = False - self._statresult = None - self._statresult_symlink = None - - def inode(self): - """Return the inode number of the entry.""" - if self._inode is None: - self.stat(follow_symlinks=False) - return self._inode - - def is_dir(self, follow_symlinks=True): - """Return True if this entry is a directory or a symbolic link - pointing to a directory; return False if the entry is or points - to any other kind of file, or if it doesn't exist anymore. - - Args: - follow_symlinks: If False, return True only if this entry is a directory - (without following symlinks) - """ - return self._isdir and (follow_symlinks or not self._islink) - - def is_file(self, follow_symlinks=True): - """Return True if this entry is a file or a symbolic link - pointing to a file; return False if the entry is or points - to a directory or other non-file entry, or if it doesn't exist anymore. - - Args: - follow_symlinks: If False, return True only if this entry is a file - (without following symlinks) - """ - return not self._isdir and (follow_symlinks or not self._islink) - - def is_symlink(self): - """Return True if this entry is a symbolic link (even if broken).""" - return self._islink - - def stat(self, follow_symlinks=True): - """Return a stat_result object for this entry. - - Args: - follow_symlinks: If False and the entry is a symlink, return the - result for the symlink, otherwise for the object is points to. - """ - if follow_symlinks: - if self._statresult_symlink is None: - stats = self._fake_os.filesystem.ResolveObject(self.path) - if _IS_WINDOWS: - # under Windows, some properties are 0 - # probably due to performance reasons - stats.st_ino = 0 - stats.st_dev = 0 - stats.st_nlink = 0 - self._statresult_symlink = os.stat_result( - (stats.st_mode, stats.st_ino, stats.st_dev, - stats.st_nlink, stats.st_uid, stats.st_gid, - stats.st_size, stats.st_atime, - stats.st_mtime, stats.st_ctime)) - return self._statresult_symlink - - if self._statresult is None: - stats = self._fake_os.filesystem.LResolveObject(self.path) - self._inode = stats.st_ino - if _IS_WINDOWS: - stats.st_ino = 0 - stats.st_dev = 0 - stats.st_nlink = 0 - self._statresult = os.stat_result( - (stats.st_mode, stats.st_ino, stats.st_dev, - stats.st_nlink, stats.st_uid, stats.st_gid, - stats.st_size, stats.st_atime, - stats.st_mtime, stats.st_ctime)) - return self._statresult - def scandir(self, path=''): """Return an iterator of DirEntry objects corresponding to the entries in the directory given by path. @@ -2343,15 +2413,7 @@ def scandir(self, path=''): Raises: OSError: if the target is not a directory """ - path = self.filesystem.ResolvePath(path) - fake_dir = self.filesystem.ConfirmDir(path) - for entry in fake_dir.contents: - dir_entry = self.DirEntry(self) - dir_entry.name = entry - dir_entry.path = self.path.join(path, dir_entry.name) - dir_entry._isdir = self.path.isdir(dir_entry.path) - dir_entry._islink = self.path.islink(dir_entry.path) - yield dir_entry + return self.filesystem.ScanDir(path) def _ClassifyDirectoryContents(self, root): """Classify contents of a directory as files/directories. @@ -2503,7 +2565,6 @@ def rename(self, old_file, new_file): """ self.filesystem.RenameObject(old_file, new_file) - if sys.version_info >= (3, 3): def replace(self, old_file, new_file): """Renames a FakeFile object at old_file to new_file, preserving all properties. diff --git a/pyfakefs/fake_filesystem_glob.py b/pyfakefs/fake_filesystem_glob.py index 436a0e89..5b0faec6 100755 --- a/pyfakefs/fake_filesystem_glob.py +++ b/pyfakefs/fake_filesystem_glob.py @@ -127,7 +127,7 @@ def _iglob(self, pathname, recursive): yield self._path_module.join(dirname, name) # These 2 helper functions non-recursively glob inside a literal directory. - # They return a list of basenames. `_glob1` accepts a pattern while `_glob0` + # They return a list of basenames. `glob1` accepts a pattern while `glob0` # takes a literal basename (so it only has to check for its existence). def glob1(self, dirname, pattern): if not dirname: diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py index e8477378..9def96b4 100644 --- a/pyfakefs/fake_pathlib.py +++ b/pyfakefs/fake_pathlib.py @@ -80,6 +80,9 @@ class _FakeAccessor(pathlib._Accessor): # pylint: disable=protected-access chmod = _wrap_strfunc(FakeFilesystem.ChangeMode) + if sys.version_info >= (3, 6): + scandir = _wrap_strfunc(FakeFilesystem.ScanDir) + if hasattr(os, "lchmod"): lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.ChangeMode( fs, path, mode, follow_symlinks=False)) diff --git a/pyfakefs/fake_tempfile.py b/pyfakefs/fake_tempfile.py index b07b5c1f..0202dc87 100644 --- a/pyfakefs/fake_tempfile.py +++ b/pyfakefs/fake_tempfile.py @@ -51,6 +51,8 @@ def __init__(self, filesystem): self.tempdir = None # initialized by mktemp(), others self._temp_prefix = 'tmp' self._mktemp_retvals = [] + # pylint: disable=protected-access + self._randomSequence = self._tempfile._RandomNameSequence() def _TempFilename(self, suffix='', prefix=None, directory=None): """Create a temporary filename that does not exist. @@ -75,11 +77,11 @@ def _TempFilename(self, suffix='', prefix=None, directory=None): if prefix is None: prefix = self._temp_prefix while not filename or self._filesystem.Exists(filename): - # pylint: disable=protected-access filename = self._filesystem.JoinPaths(directory, '%s%s%s' % ( prefix, - next(self._tempfile._RandomNameSequence()), + next(self._randomSequence), suffix)) + print(filename) return filename # pylint: disable=redefined-builtin,unused-argument,too-many-arguments From 85565a89340f400b420e508c232fbcbdbd9b5ca2 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Tue, 8 Nov 2016 20:16:44 +0100 Subject: [PATCH 06/10] Removed debug code --- pyfakefs/fake_tempfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyfakefs/fake_tempfile.py b/pyfakefs/fake_tempfile.py index 0202dc87..39407c5e 100644 --- a/pyfakefs/fake_tempfile.py +++ b/pyfakefs/fake_tempfile.py @@ -81,7 +81,6 @@ def _TempFilename(self, suffix='', prefix=None, directory=None): prefix, next(self._randomSequence), suffix)) - print(filename) return filename # pylint: disable=redefined-builtin,unused-argument,too-many-arguments From 62b5c052e5e43c3f86864503245adb12d2c68be1 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Wed, 9 Nov 2016 19:45:17 +0100 Subject: [PATCH 07/10] Added handling for path-like objects for some functions - many functions in 3.6 can take a path-like object, which can be transformed to a path by os.fspath() --- pyfakefs/fake_filesystem.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index c6e62431..68889c68 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -822,6 +822,8 @@ def CollapsePath(self, path): Returns: (str) A copy of path with empty components and dot components removed. """ + if sys.version_info >= (3, 6): + path = os.fspath(path) path = self.NormalizePathSeparator(path) drive, path = self.SplitDrive(path) is_absolute_path = path.startswith(self.path_separator) @@ -910,6 +912,8 @@ def SplitPath(self, path): (str) A duple (pathname, basename) for which pathname does not end with a slash, and basename does not contain a slash. """ + if sys.version_info >= (3, 6): + path = os.fspath(path) drive, path = self.SplitDrive(path) path = self.NormalizePathSeparator(path) path_components = path.split(self.path_separator) @@ -934,6 +938,8 @@ def SplitDrive(self, path): original path. Taken from Windows specific implementation in Python 3.5 and slightly adapted. """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if self.supports_drive_letter: if len(path) >= 2: path = self.NormalizePathSeparator(path) @@ -2019,6 +2025,8 @@ def isabs(self, path): """Return True if path is an absolute pathname.""" if self.filesystem.supports_drive_letter: path = self.splitdrive(path)[1] + if sys.version_info >= (3, 6): + path = os.fspath(path) if _IS_WINDOWS: return len(path) > 0 and path[0] in (self.sep, self.altsep) else: @@ -2065,6 +2073,8 @@ def getcwd(): else: return self.os.getcwd() + if sys.version_info >= (3, 6): + path = os.fspath(path) if not self.isabs(path): path = self.join(getcwd(), path) elif (self.filesystem.supports_drive_letter and From 0e16f393fd00cade90c15a9245de61f6483f0984 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Wed, 9 Nov 2016 19:51:43 +0100 Subject: [PATCH 08/10] Adapted relpath error handling to behave like real function --- fake_filesystem_test.py | 5 +---- pyfakefs/fake_filesystem.py | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fake_filesystem_test.py b/fake_filesystem_test.py index 62e60629..e222df9f 100755 --- a/fake_filesystem_test.py +++ b/fake_filesystem_test.py @@ -2460,10 +2460,7 @@ def testRelpath(self): path_foo = '/path/to/foo' path_bar = '/path/to/bar' path_other = '/some/where/else' - if sys.version_info >= (3, 6): - self.assertRaises(TypeError, self.path.relpath, None) - else: - self.assertRaises(ValueError, self.path.relpath, None) + self.assertRaises(ValueError, self.path.relpath, None) self.assertRaises(ValueError, self.path.relpath, '') if sys.version_info < (2, 7): # The real Python 2.6 os.path.relpath('/path/to/foo') actually does diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 68889c68..2a2b1015 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -2108,6 +2108,10 @@ def normcase(self, path): def relpath(self, path, start=None): """ntpath.relpath() needs the cwd passed in the start argument.""" + if not path: + raise ValueError("no path specified") + if sys.version_info >= (3, 6): + path = os.fspath(path) if start is None: start = self.filesystem.cwd path = self._os_path.relpath(path, start) From d3dda7aa95ddc2da77077e92f8c55ea38b8ccd27 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Fri, 11 Nov 2016 21:03:57 +0100 Subject: [PATCH 09/10] Added support for path-like objects in Python 3.6 - added tests for most faked functions that accepts a path-like object in 3.6 - adapted the code respectively by calling os.fspath() where needed - added getatime() and getctime() to fake os module --- fake_pathlib_test.py | 233 +++++++++++++++++++++++++++++++++++- pyfakefs/fake_filesystem.py | 59 +++++++-- 2 files changed, 283 insertions(+), 9 deletions(-) diff --git a/fake_pathlib_test.py b/fake_pathlib_test.py index 0d15d198..bca3b516 100644 --- a/fake_pathlib_test.py +++ b/fake_pathlib_test.py @@ -64,7 +64,9 @@ def test_init_with_segments(self): self.path('/etc/init.d/reboot')) def test_init_collapse(self): - """Tests for collapsing path during initialization - taken from pathlib.PurePath documentation""" + """Tests for collapsing path during initialization. + Taken from pathlib.PurePath documentation. + """ self.assertEqual(self.path('foo//bar'), self.path('foo/bar')) self.assertEqual(self.path('foo/./bar'), self.path('foo/bar')) self.assertNotEqual(self.path('foo/../bar'), self.path('foo/bar')) @@ -104,7 +106,9 @@ def test_init_with_segments(self): self.assertEqual(self.path('c:/Users') / 'john' / 'data', self.path('c:/Users/john/data')) def test_init_collapse(self): - """Tests for collapsing path during initialization - taken from pathlib.PurePath documentation""" + """Tests for collapsing path during initialization. + Taken from pathlib.PurePath documentation. + """ self.assertEqual(self.path('c:/Windows', 'd:bar'), self.path('d:bar')) self.assertEqual(self.path('c:/Windows', '/Program Files'), self.path('c:/Program Files')) @@ -453,5 +457,230 @@ def test_glob(self): self.assertEqual(sorted(path.glob('*.py')), [self.path('/foo/all_tests.py'), self.path('/foo/setup.py')]) + +@unittest.skipIf(sys.version_info < (3, 6), 'path-like objects new in Python 3.6') +class FakePathlibUsageInOsFunctionsTest(unittest.TestCase): + """Test that many os / os.path functions accept a path-like object since Python 3.6. + The functionality of these functions is testd elsewhere, we just check that they + accept a fake path object as an argument. + """ + + def setUp(self): + self.filesystem = fake_filesystem.FakeFilesystem(path_separator='/') + self.pathlib = fake_pathlib.FakePathlibModule(self.filesystem) + self.path = self.pathlib.Path + self.os = fake_filesystem.FakeOsModule(self.filesystem) + + def test_join(self): + dir1 = 'foo' + dir2 = 'bar' + dir = self.os.path.join(dir1, dir2) + self.assertEqual(dir, self.os.path.join(self.path(dir1), dir2)) + self.assertEqual(dir, self.os.path.join(dir1, self.path(dir2))) + self.assertEqual(dir, self.os.path.join(self.path(dir1), self.path(dir2))) + + def test_normcase(self): + dir1 = '/Foo/Bar/Baz' + self.assertEqual(self.os.path.normcase(dir1), self.os.path.normcase(self.path(dir1))) + + def test_normpath(self): + dir1 = '/foo/bar/../baz' + self.assertEqual(self.os.path.normpath(dir1), self.os.path.normpath(self.path(dir1))) + + def test_realpath(self): + dir1 = '/foo/bar/../baz' + self.assertEqual(self.os.path.realpath(dir1), self.os.path.realpath(self.path(dir1))) + + def test_relpath(self): + path_foo = '/path/to/foo' + path_bar = '/path/to/bar' + rel_path = self.os.path.relpath(path_foo, path_bar) + self.assertEqual(rel_path, self.os.path.relpath(self.path(path_foo), path_bar)) + self.assertEqual(rel_path, self.os.path.relpath(path_foo, self.path(path_bar))) + self.assertEqual(rel_path, self.os.path.relpath(self.path(path_foo), self.path(path_bar))) + + def test_split(self): + dir1 = '/Foo/Bar/Baz' + self.assertEqual(self.os.path.split(dir1), self.os.path.split(self.path(dir1))) + + def test_splitdrive(self): + dir1 = 'C:/Foo/Bar/Baz' + self.assertEqual(self.os.path.splitdrive(dir1), self.os.path.splitdrive(self.path(dir1))) + + def test_abspath(self): + dir1 = '/foo/bar/../baz' + self.assertEqual(self.os.path.abspath(dir1), self.os.path.abspath(self.path(dir1))) + + def test_exists(self): + dir1 = '/foo/bar/../baz' + self.assertEqual(self.os.path.exists(dir1), self.os.path.exists(self.path(dir1))) + + def test_lexists(self): + dir1 = '/foo/bar/../baz' + self.assertEqual(self.os.path.lexists(dir1), self.os.path.lexists(self.path(dir1))) + + def test_expanduser(self): + dir1 = '~/foo' + self.assertEqual(self.os.path.expanduser(dir1), self.os.path.expanduser(self.path(dir1))) + + def test_getmtime(self): + dir1 = 'foo/bar1.txt' + path_obj = self.filesystem.CreateFile(dir1) + path_obj.SetMTime(24) + self.assertEqual(self.os.path.getmtime(dir1), self.os.path.getmtime(self.path(dir1))) + + def test_getctime(self): + dir1 = 'foo/bar1.txt' + path_obj = self.filesystem.CreateFile(dir1) + path_obj.SetCTime(42) + self.assertEqual(self.os.path.getctime(dir1), self.os.path.getctime(self.path(dir1))) + + def test_getatime(self): + dir1 = 'foo/bar1.txt' + path_obj = self.filesystem.CreateFile(dir1) + path_obj.SetATime(11) + self.assertEqual(self.os.path.getatime(dir1), self.os.path.getatime(self.path(dir1))) + + def test_getsize(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path, contents='1234567') + self.assertEqual(self.os.path.getsize(path), self.os.path.getsize(self.path(path))) + + def test_isabs(self): + path = '/foo/bar/../baz' + self.assertEqual(self.os.path.isabs(path), self.os.path.isabs(self.path(path))) + + def test_isfile(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path) + self.assertEqual(self.os.path.isfile(path), self.os.path.isfile(self.path(path))) + + def test_islink(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path) + self.assertEqual(self.os.path.islink(path), self.os.path.islink(self.path(path))) + + def test_isdir(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path) + self.assertEqual(self.os.path.isdir(path), self.os.path.isdir(self.path(path))) + + def test_ismount(self): + path = '/' + self.assertEqual(self.os.path.ismount(path), self.os.path.ismount(self.path(path))) + + def test_access(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path, contents='1234567') + self.assertEqual(self.os.access(path, os.R_OK), self.os.access(self.path(path), os.R_OK)) + + def test_chdir(self): + path = '/foo/bar/baz' + self.filesystem.CreateDirectory(path) + self.os.chdir(self.path(path)) + self.assertEqual(path, self.filesystem.cwd) + + def test_chmod(self): + path = '/some_file' + self.filesystem.CreateFile(path) + self.os.chmod(self.path(path), 0o400) + self.assertEqual(stat.S_IMODE(0o400), stat.S_IMODE(self.os.stat(path).st_mode)) + + def test_link(self): + file1_path = 'test_file1' + file2_path = 'test_file2' + self.filesystem.CreateFile(file1_path) + self.os.link(self.path(file1_path), file2_path) + self.assertTrue(self.os.path.exists(file2_path)) + self.os.unlink(file2_path) + self.os.link(self.path(file1_path), self.path(file2_path)) + self.assertTrue(self.os.path.exists(file2_path)) + self.os.unlink(file2_path) + self.os.link(file1_path, self.path(file2_path)) + self.assertTrue(self.os.path.exists(file2_path)) + + def test_listdir(self): + path = '/foo/bar' + self.filesystem.CreateDirectory(path) + self.filesystem.CreateFile(path + 'baz.txt') + self.assertEqual(self.os.listdir(path), self.os.listdir(self.path(path))) + + def test_mkdir(self): + path = '/foo' + self.os.mkdir(self.path(path)) + self.assertTrue(self.filesystem.Exists(path)) + + def test_makedirs(self): + path = '/foo/bar' + self.os.makedirs(self.path(path)) + self.assertTrue(self.filesystem.Exists(path)) + + def test_readlink(self): + link_path = 'foo/bar/baz' + target = 'tarJAY' + self.filesystem.CreateLink(link_path, target) + self.assertEqual(self.os.readlink(self.path(link_path)), target) + + def test_remove(self): + path = '/test.txt' + self.filesystem.CreateFile(path) + self.os.remove(self.path(path)) + self.assertFalse(self.filesystem.Exists(path)) + + def test_rename(self): + path1 = 'test1.txt' + path2 = 'test2.txt' + self.filesystem.CreateFile(path1) + self.os.rename(self.path(path1), path2) + self.assertTrue(self.filesystem.Exists(path2)) + self.os.rename(self.path(path2), self.path(path1)) + self.assertTrue(self.filesystem.Exists(path1)) + + def test_replace(self): + path1 = 'test1.txt' + path2 = 'test2.txt' + self.filesystem.CreateFile(path1) + self.os.replace(self.path(path1), path2) + self.assertTrue(self.filesystem.Exists(path2)) + self.os.replace(self.path(path2), self.path(path1)) + self.assertTrue(self.filesystem.Exists(path1)) + + def test_rmdir(self): + path = '/foo/bar' + self.filesystem.CreateDirectory(path) + self.os.rmdir(self.path(path)) + self.assertFalse(self.filesystem.Exists(path)) + + def test_scandir(self): + directory = '/xyzzy/plugh' + self.filesystem.CreateDirectory(directory) + self.filesystem.CreateFile(directory + '/test.txt') + dir_entries = [entry for entry in self.os.scandir(self.path(directory))] + self.assertEqual(1, len(dir_entries)) + + def test_symlink(self): + file_path = 'test_file1' + link_path = 'link' + self.filesystem.CreateFile(file_path) + self.os.symlink(self.path(file_path), link_path) + self.assertTrue(self.os.path.exists(link_path)) + self.os.remove(link_path) + self.os.symlink(self.path(file_path), self.path(link_path)) + self.assertTrue(self.os.path.exists(link_path)) + + def test_stat(self): + path = 'foo/bar/baz' + self.filesystem.CreateFile(path, contents='1234567') + self.assertEqual(self.os.stat(path, os.R_OK), self.os.stat(self.path(path), os.R_OK)) + + def test_utime(self): + path = '/some_file' + self.filesystem.CreateFile(path, contents='test') + self.os.utime(self.path(path), (1, 2)) + st = self.os.stat(path) + self.assertEqual(1, st.st_atime) + self.assertEqual(2, st.st_mtime) + + if __name__ == '__main__': unittest.main() diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 2a2b1015..d72dfdac 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -370,7 +370,7 @@ def SetATime(self, st_atime): """Set the self.st_atime attribute. Args: - st_atime: The desired atime. + st_atime: The desired access time. """ self.st_atime = st_atime @@ -378,10 +378,18 @@ def SetMTime(self, st_mtime): """Set the self.st_mtime attribute. Args: - st_mtime: The desired mtime. + st_mtime: The desired modification time. """ self.st_mtime = st_mtime + def SetCTime(self, st_ctime): + """Set the self.st_ctime attribute. + + Args: + st_ctime: The desired creation time. + """ + self.st_ctime = st_ctime + def __str__(self): return '%s(%o)' % (self.name, self.st_mode) @@ -798,6 +806,8 @@ def NormalizePathSeparator(self, path): Returns: The normalized path that will be used internally. """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if self.alternative_path_separator is None or not path: return path return path.replace(self.alternative_path_separator, self.path_separator) @@ -822,8 +832,6 @@ def CollapsePath(self, path): Returns: (str) A copy of path with empty components and dot components removed. """ - if sys.version_info >= (3, 6): - path = os.fspath(path) path = self.NormalizePathSeparator(path) drive, path = self.SplitDrive(path) is_absolute_path = path.startswith(self.path_separator) @@ -912,8 +920,6 @@ def SplitPath(self, path): (str) A duple (pathname, basename) for which pathname does not end with a slash, and basename does not contain a slash. """ - if sys.version_info >= (3, 6): - path = os.fspath(path) drive, path = self.SplitDrive(path) path = self.NormalizePathSeparator(path) path_components = path.split(self.path_separator) @@ -1006,6 +1012,7 @@ def JoinPaths(self, *paths): (str) The paths joined by the path separator, starting with the last absolute path in paths. """ + paths = [os.fspath(path) for path in paths] if len(paths) == 1: return paths[0] if self.supports_drive_letter: @@ -1102,6 +1109,8 @@ def Exists(self, file_path): Raises: TypeError: if file_path is None """ + if sys.version_info >= (3, 6): + file_path = os.fspath(file_path) if file_path is None: raise TypeError if not file_path: @@ -1204,6 +1213,8 @@ def _FollowLink(link_path_components, link): # Don't call self.NormalizePath(), as we don't want to prepend self.cwd. return self.CollapsePath(link_path) + if sys.version_info >= (3, 6): + file_path = os.fspath(file_path) if file_path is None: # file.open(None) raises TypeError, so mimic that. raise TypeError('Expected file system path string, received None') @@ -1267,6 +1278,8 @@ def GetObjectFromNormalizedPath(self, file_path): Raises: IOError: if the object is not found """ + if sys.version_info >= (3, 6): + file_path = os.fspath(file_path) if file_path == self.root.name: return self.root path_components = self.GetPathComponents(file_path) @@ -1296,6 +1309,8 @@ def GetObject(self, file_path): Raises: IOError: if the object is not found """ + if sys.version_info >= (3, 6): + file_path = os.fspath(file_path) file_path = self.NormalizePath(self.NormalizeCase(file_path)) return self.GetObjectFromNormalizedPath(file_path) @@ -1313,6 +1328,8 @@ def ResolveObject(self, file_path, follow_symlinks=True): IOError: if the object is not found """ if follow_symlinks: + if sys.version_info >= (3, 6): + file_path = os.fspath(file_path) return self.GetObjectFromNormalizedPath(self.ResolvePath(file_path)) return self.LResolveObject(file_path) @@ -1331,6 +1348,8 @@ def LResolveObject(self, path): Raises: IOError: if the object is not found """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if path == self.root.name: # The root directory will never be a link return self.root @@ -1569,6 +1588,8 @@ def CreateLink(self, file_path, link_target, target_is_directory=False): if not _IS_LINK_SUPPORTED: raise OSError("Symbolic links are not supported on Windows before Python 3.2") resolved_file_path = self.ResolvePath(file_path) + if sys.version_info >= (3, 6): + link_target = os.fspath(link_target) return self.CreateFile(resolved_file_path, st_mode=stat.S_IFLNK | PERM_DEF, contents=link_target) @@ -1629,6 +1650,8 @@ def MakeDirectory(self, dir_name, mode=PERM_DEF): OSError: if the directory name is invalid or parent directory is read only or as per FakeFilesystem.AddObject. """ + if sys.version_info >= (3, 6): + dir_name = os.fspath(dir_name) if self._EndsWithPathSeparator(dir_name): dir_name = dir_name[:-1] @@ -1702,6 +1725,8 @@ def _IsType(self, path, st_flag): Raises: TypeError: if path is None """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if path is None: raise TypeError try: @@ -1732,6 +1757,8 @@ def IsLink(self, path): Raises: TypeError: if path is None """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if path is None: raise TypeError try: @@ -2056,13 +2083,29 @@ def islink(self, path): return self.filesystem.IsLink(path) def getmtime(self, path): - """Returns the mtime of the file.""" + """Returns the modification time of the file.""" try: file_obj = self.filesystem.GetObject(path) except IOError as exc: raise OSError(errno.ENOENT, str(exc)) return file_obj.st_mtime + def getatime(self, path): + """Returns the access time of the file.""" + try: + file_obj = self.filesystem.GetObject(path) + except IOError as exc: + raise OSError(errno.ENOENT, str(exc)) + return file_obj.st_atime + + def getctime(self, path): + """Returns the creation time of the file.""" + try: + file_obj = self.filesystem.GetObject(path) + except IOError as exc: + raise OSError(errno.ENOENT, str(exc)) + return file_obj.st_ctime + def abspath(self, path): """Return the absolute version of a path.""" @@ -2135,6 +2178,8 @@ def ismount(self, path): True if path is a mount point added to the fake file system. Under Windows also returns True for drive and UNC roots (independent of their existence). """ + if sys.version_info >= (3, 6): + path = os.fspath(path) if not path: return False normed_path = self.filesystem.NormalizePath(path) From 78a30660b91dc76944c3e8fc94c486c1a62a6646 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Fri, 11 Nov 2016 21:09:41 +0100 Subject: [PATCH 10/10] Added missing version check --- pyfakefs/fake_filesystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index d72dfdac..703dc3ac 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -1012,7 +1012,8 @@ def JoinPaths(self, *paths): (str) The paths joined by the path separator, starting with the last absolute path in paths. """ - paths = [os.fspath(path) for path in paths] + if sys.version_info >= (3, 6): + paths = [os.fspath(path) for path in paths] if len(paths) == 1: return paths[0] if self.supports_drive_letter: