From 8fac6930861247edaa5df0e102f99c6449d8035b Mon Sep 17 00:00:00 2001 From: Martin Larralde Date: Sat, 10 Jun 2017 19:06:35 +0200 Subject: [PATCH] Allow fs to be extended with subpackages (#47) * Make fs a namespace package * Make fs.opener a namespace package and expand opener.py into several files * Remove opener.py module * Make manage_fs a Registry instance method * Fix tests.test_opener to expect opener.registry.ParseResult * Add a separate file for each fs Opener * Use fs.path instead of os.path in fs.opener.osfs * Add auto fs.opener.* modules loader * Add a dynamically defined __all__ to fs.opener * Make fs.opener.Opener an Abstract Base Class * Add small docstring to every fs.opener submodule * Add documentation about how to create a fs extension * Revert back to os.path in OSFSOpener to fix potential issues on win paths --- docs/source/extension.rst | 104 +++++++++ docs/source/index.rst | 4 +- docs/source/reference/opener.rst | 10 +- fs/__init__.py | 5 +- fs/opener.py | 380 ------------------------------- fs/opener/__init__.py | 44 ++++ fs/opener/_base.py | 47 ++++ fs/opener/_errors.py | 18 ++ fs/opener/_registry.py | 253 ++++++++++++++++++++ fs/opener/ftpfs.py | 27 +++ fs/opener/memoryfs.py | 14 ++ fs/opener/osfs.py | 15 ++ fs/opener/tarfs.py | 17 ++ fs/opener/tempfs.py | 14 ++ fs/opener/zipfs.py | 17 ++ tests/test_opener.py | 8 +- 16 files changed, 588 insertions(+), 389 deletions(-) create mode 100644 docs/source/extension.rst delete mode 100644 fs/opener.py create mode 100644 fs/opener/__init__.py create mode 100644 fs/opener/_base.py create mode 100644 fs/opener/_errors.py create mode 100644 fs/opener/_registry.py create mode 100644 fs/opener/ftpfs.py create mode 100644 fs/opener/memoryfs.py create mode 100644 fs/opener/osfs.py create mode 100644 fs/opener/tarfs.py create mode 100644 fs/opener/tempfs.py create mode 100644 fs/opener/zipfs.py diff --git a/docs/source/extension.rst b/docs/source/extension.rst new file mode 100644 index 00000000..8c3d78a8 --- /dev/null +++ b/docs/source/extension.rst @@ -0,0 +1,104 @@ +.. _extension: + +Creating an extension +===================== + +Once an new filesystem implemented, it is possible to distribute as a +subpackage contained in the ``fs`` namespace. Let's say you are trying +to create an extension for a filesystem called **AwesomeFS**. + + +Name +---- + +For the sake of clarity, and to give a clearer sight of the +Pyfilesystem2 ecosystem, your extension should be called **fs.awesome** +or **fs.awesomefs**, since PyPI allows packages to be namespaced. Let us +stick with **fs.awesome** for now. + + +Structure +--------- + +The extension must have either of the following structures: :: + + └── fs.awesome └── fs.awesome + ├── fs ├── fs + │ ├── awesomefs.py │ ├── awesomefs + │ └── opener | | ├── __init__.py + │ └── awesomefs.py | | ├── some_file.py + └── setup.py | | └── some_other_file.py + │ └── opener + │ └── awesomefs.py + └── setup.py + + +The structure on the left will work fine if you only need a single file +to implement **AwesomeFS**, but if you end up creating more, +you should probably use the structure on the right (create a package +instead of a single file). + +.. warning :: + + Do **NOT** create ``fs/__init__.py`` or ``fs/opener/__init__.py`` ! Since + those files are vital to the main Pyfilesystem2 package, including them + could result in having your extension break the whole Pyfilesystem2 + package when installing. + + +``setup.py`` +------------ + +Refer to the `setuptools documentation `_ +to see how to write a ``setup.py`` file. There are only a few things that +should be kept in mind when creating a Pyfilesystem2 extension. Make sure that: + +* the name of the package is the *namespaced* name (**fs.awesome** with our + example). +* ``fs``, ``fs.opener`` and ``fs.awesomefs`` packages are included. Since + you can't create ``fs/__init__.py`` and ``fs/opener/__init__.py``, setuptools + won't be able to find your packages if you use ``setuptools.find_packages``, + so you will have to include packages manually. +* ``fs`` is in the ``install_requires`` list, in order to + always have Pyfilesystem2 installed before your extension. + + +Opener +------ + +To ensure your new filesystem can be reached through the generic ``fs.open_fs`` method, +you must declare a :class:`~fs.opener._base.Opener` in the ``fs/opener`` directory. With our example, +create a file called ``awesomefs.py`` containing the definition of ``AwesomeOpener`` +or ``AwesomeFSOpener`` inside of the ``fs/opener`` directory. This will +allow your Filesystem to be created directly through ``fs.open_fs``, without +having to import your extension first ! + + +Practices +--------- + +* Use relative imports whenever you try to access to a resource in the + ``fs`` module or any of its submodules. +* Keep track of your achievements ! Add ``__version__``, ``__author__``, + ``__author_email__`` and ``__license__`` variables to your project + (either in ``fs/awesomefs.py`` or ``fs/awesomefs/__init__.py`` depending + on the chosen structure), containing: + + ``__version__`` + the version of the extension (use `Semantic Versioning `_ if possible !) + + ``__author__`` + your name(s) + + ``__author_email__`` + your email(s) + + ``__license__`` + the license of the subpackage + + +Example +------- + +See `fs.sshfs `_ for a functioning +PyFilesystem2 extension implementing the SFTP protocol. diff --git a/docs/source/index.rst b/docs/source/index.rst index 9c47a84f..cd007d78 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,8 +18,9 @@ Contents: openers.rst walking.rst builtin.rst - external.rst implementers.rst + extension.rst + external.rst reference.rst @@ -30,4 +31,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/source/reference/opener.rst b/docs/source/reference/opener.rst index e7ded649..2c0f0262 100644 --- a/docs/source/reference/opener.rst +++ b/docs/source/reference/opener.rst @@ -3,5 +3,11 @@ fs.opener Open filesystems from a URL. -.. automodule:: fs.opener - :members: \ No newline at end of file +.. automodule:: fs.opener._base + :members: + +.. automodule:: fs.opener._registry + :members: + +.. automodule:: fs.opener._errors + :members: diff --git a/fs/__init__.py b/fs/__init__.py index a7cf9435..a8d44b47 100644 --- a/fs/__init__.py +++ b/fs/__init__.py @@ -1,3 +1,6 @@ +from pkgutil import extend_path +__path__ = extend_path(__path__, __name__) + from ._version import __version__ from .enums import ResourceType, Seek -from .opener import open_fs \ No newline at end of file +from .opener import open_fs diff --git a/fs/opener.py b/fs/opener.py deleted file mode 100644 index ae3e04ad..00000000 --- a/fs/opener.py +++ /dev/null @@ -1,380 +0,0 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -from contextlib import contextmanager -import os -import re - -from collections import namedtuple - - -ParseResult = namedtuple( - 'ParseResult', - [ - 'protocol', - 'username', - 'password', - 'resource', - 'path' - ] -) - - -_RE_FS_URL = re.compile(r''' -^ -(.*?) -:\/\/ - -(?: -(?:(.*?)@(.*?)) -|(.*?) -) - -(?: -!(.*?)$ -)*$ -''', re.VERBOSE) - - -@contextmanager -def manage_fs(fs_url, create=False, writeable=True, cwd='.'): - ''' - A context manager opens / closes a filesystem. - - :param fs_url: A FS instance or a FS URL. - :type fs_url: str or FS - :param bool create: If ``True``, then create the filesytem if it - doesn't already exist. - :param bool writeable: If ``True``, then the filesystem should be - writeable. - :param str cwd: The current working directory, if opening a - :class:`~fs.osfs.OSFS`. - - Sometimes it is convenient to be able to pass either a FS object - *or* an FS URL to a function. This context manager handles the - required logic for that. - - Here's an example:: - - def print_ls(list_fs): - """List a directory.""" - with manage_fs(list_fs) as fs: - print(" ".join(fs.listdir())) - - This function may be used in two ways. You may either pass either a - ``str``, as follows:: - - print_list('zip://projects.zip') - - Or, an FS instance:: - - from fs.osfs import OSFS - projects_fs = OSFS('~/') - print_list(projects_fs) - - ''' - from .base import FS - if isinstance(fs_url, FS): - yield fs_url - else: - _fs = open_fs( - fs_url, - create=create, - writeable=writeable, - cwd=cwd - ) - try: - yield _fs - except: - raise - finally: - _fs.close() - - -class ParseError(ValueError): - """Raised when attempting to parse an invalid FS URL.""" - - -class OpenerError(Exception): - """Base class for opener related errors.""" - - -class Unsupported(OpenerError): - """May be raised by opener if the opener fails to open a FS.""" - - -def parse(fs_url): - """ - Parse a Filesystem URL and return a :class:`ParseResult`, or raise - :class:`ParseError` (subclass of ValueError) if the FS URL is - not value. - - :param fs_url: A filesystem URL - :type fs_url: str - :rtype: :class:`ParseResult` - - """ - match = _RE_FS_URL.match(fs_url) - if match is None: - raise ParseError('{!r} is not a fs2 url'.format(fs_url)) - - fs_name, credentials, url1, url2, path = match.groups() - if credentials: - username, _, password = credentials.partition(':') - url = url1 - else: - username = None - password = None - url = url2 - return ParseResult( - fs_name, - username, - password, - url, - path - ) - - - - -class Opener(object): - """ - The opener base class. - - An opener is responsible for opening a filesystems from one or more - protocols. A list of supported protocols is supplied in a class - attribute called `protocols`. - - Openers should be registered with a :class:`~fs.opener.Registry` - object, which picks an appropriate opener object for a given FS URL. - - """ - - protocols = [] - - def __repr__(self): - return "".format(self.protocols) - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - """ - Open a filesystem object from a FS URL. - - :param str fs_url: A filesystem URL - :param parse_result: A parsed filesystem URL. - :type parse_result: :class:`ParseResult` - :param bool writeable: True if the filesystem must be writeable. - :param bool create: True if the filesystem should be created if - it does not exist. - :param str cwd: The current working directory (generally only - relevant for OS filesystems). - :returns: :class:`~fs.base.FS` object - - """ - - -class Registry(object): - """ - A registry for `Opener` instances. - - """ - - def __init__(self, default_opener='osfs'): - """ - Create a registry object. - - :param default_opener: The protocol to use, if one is not - supplied. The default is to use 'osfs', so that the FS URL - is treated as a system path if no protocol is given. - - """ - self.default_opener = default_opener - self.protocols = {} - - def install(self, opener): - """ - Install an opener. - - :param opener: An :class:`Opener` instance, or a callable - that returns an opener instance. - - May be used as a class decorator. For example:: - - registry = Registry() - - @registry.install - class ArchiveOpener(Opener): - protocols = ['zip', 'tar'] - - """ - if not isinstance(opener, Opener): - opener = opener() - assert opener.protocols, "must list one or more protocols" - for protocol in opener.protocols: - self.protocols[protocol] = opener - - def open(self, - fs_url, - writeable=True, - create=False, - cwd=".", - default_protocol='osfs'): - """ - Open a filesystem from a FS URL. Returns a tuple of a filesystem - object and a path. If there is no path in the FS URL, the path - value will be ``None``. - - :param str fs_url: A filesystem URL - :param bool writeable: True if the filesystem must be writeable. - :param bool create: True if the filesystem should be created if - it does not exist. - :param cwd: The current working directory. - :type cwd: str or None - :rtype: Tuple of ``(, )`` - - """ - - if '://' not in fs_url: - # URL may just be a path - fs_url = "{}://{}".format(default_protocol, fs_url) - - parse_result = parse(fs_url) - protocol = parse_result.protocol - open_path = parse_result.path - - opener = self.protocols.get(protocol, None) - - if not opener: - raise Unsupported( - "protocol '{}' is not supported".format(protocol) - ) - - open_fs = opener.open_fs( - fs_url, - parse_result, - writeable, - create, - cwd - ) - return open_fs, open_path - - def open_fs(self, - fs_url, - writeable=True, - create=False, - cwd=".", - default_protocol='osfs'): - """ - Open a filesystem object from a FS URL (ignoring the path - component). - - :param str fs_url: A filesystem URL - :param parse_result: A parsed filesystem URL. - :type parse_result: :class:`ParseResult` - :param bool writeable: True if the filesystem must be writeable. - :param bool create: True if the filesystem should be created if - it does not exist. - :param str cwd: The current working directory (generally only - relevant for OS filesystems). - :param str default_protocol: The protocol to use if one is not - supplied in the FS URL (defaults to ``"osfs"``). - :returns: :class:`~fs.base.FS` object - - """ - from .base import FS - if isinstance(fs_url, FS): - _fs = fs_url - else: - _fs, _path = self.open( - fs_url, - writeable=writeable, - create=create, - cwd=cwd, - default_protocol=default_protocol - ) - return _fs - - -registry = Registry() -open_fs = registry.open_fs -open = registry.open - - -@registry.install -class OSFSOpener(Opener): - protocols = ['file', 'osfs'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .osfs import OSFS - _path = os.path.abspath(os.path.join(cwd, parse_result.resource)) - path = os.path.normpath(_path) - osfs = OSFS(path, create=create) - return osfs - - -@registry.install -class TempOpener(Opener): - protocols = ['temp'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .tempfs import TempFS - temp_fs = TempFS(identifier=parse_result.resource) - return temp_fs - - -@registry.install -class MemOpener(Opener): - protocols = ['mem'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .memoryfs import MemoryFS - mem_fs = MemoryFS() - return mem_fs - - -@registry.install -class ZipOpener(Opener): - protocols = ['zip'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .zipfs import ZipFS - zip_fs = ZipFS( - parse_result.resource, - write=create - ) - return zip_fs - -@registry.install -class TarOpener(Opener): - protocols = ['tar'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .tarfs import TarFS - tar_fs = TarFS( - parse_result.resource, - write=create - ) - return tar_fs - - -@registry.install -class FTPOpener(Opener): - protocols = ['ftp'] - - def open_fs(self, fs_url, parse_result, writeable, create, cwd): - from .ftpfs import FTPFS - ftp_host, _, dir_path = parse_result.resource.partition('/') - ftp_host, _, ftp_port = ftp_host.partition(':') - ftp_port = int(ftp_port) if ftp_port.isdigit() else 21 - ftp_fs = FTPFS( - ftp_host, - port=ftp_port, - user=parse_result.username, - passwd=parse_result.password, - ) - ftp_fs = ( - ftp_fs.opendir(dir_path) - if dir_path else - ftp_fs - ) - return ftp_fs diff --git a/fs/opener/__init__.py b/fs/opener/__init__.py new file mode 100644 index 00000000..4a6d8747 --- /dev/null +++ b/fs/opener/__init__.py @@ -0,0 +1,44 @@ +# coding: utf-8 +""" +fs.opener +======== + +Imported at the same time as PyFilesystem2, contains +various objects and functions to open and manage FS. +""" + +import importlib + +# Declares fs.opener as a namespace package +import pkgutil +__path__ = pkgutil.extend_path(__path__, __name__) + +# Import objects into fs.opener namespace +from ._registry import registry, Registry +from ._errors import OpenerError, ParseError, Unsupported + +# Create a partial __all__ with imports and aliases +__all__ = [ + "registry", + "Registry", + "OpenerError", + "ParseError", + "Unsupported", + 'open_fs', + 'open', + 'manage_fs', + 'parse', +] + +# Alias functions defined as Registry methods +open_fs = registry.open_fs +open = registry.open +manage_fs = registry.manage_fs +parse = registry.parse + +# Import any file in the opener directory not prefixed by an underscore +# and add its name to the __all__ list when successful +for _, modname, _ in pkgutil.iter_modules(__path__): + if not modname.startswith('_'): + importlib.import_module('.'.join([__name__, modname]), package=__name__) + __all__.append(modname) diff --git a/fs/opener/_base.py b/fs/opener/_base.py new file mode 100644 index 00000000..8a9d2bde --- /dev/null +++ b/fs/opener/_base.py @@ -0,0 +1,47 @@ +# coding: utf-8 +""" +fs.opener._base +=============== + +Defines the Opener abstract base class. +""" + +import six +import abc + + +@six.add_metaclass(abc.ABCMeta) +class Opener(object): + """ + The opener base class. + + An opener is responsible for opening a filesystems from one or more + protocols. A list of supported protocols is supplied in a class + attribute called `protocols`. + + Openers should be registered with a :class:`~fs.opener.Registry` + object, which picks an appropriate opener object for a given FS URL. + + """ + + protocols = [] + + def __repr__(self): + return "".format(self.protocols) + + @abc.abstractmethod + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + """ + Open a filesystem object from a FS URL. + + :param str fs_url: A filesystem URL + :param parse_result: A parsed filesystem URL. + :type parse_result: :class:`ParseResult` + :param bool writeable: True if the filesystem must be writeable. + :param bool create: True if the filesystem should be created if + it does not exist. + :param str cwd: The current working directory (generally only + relevant for OS filesystems). + :returns: :class:`~fs.base.FS` object + + """ diff --git a/fs/opener/_errors.py b/fs/opener/_errors.py new file mode 100644 index 00000000..69713d5d --- /dev/null +++ b/fs/opener/_errors.py @@ -0,0 +1,18 @@ +# coding: utf-8 +""" +fs.opener._errors +================= + +Errors raised when attempting to open a filesystem +""" + +class ParseError(ValueError): + """Raised when attempting to parse an invalid FS URL.""" + + +class OpenerError(Exception): + """Base class for opener related errors.""" + + +class Unsupported(OpenerError): + """May be raised by opener if the opener fails to open a FS.""" diff --git a/fs/opener/_registry.py b/fs/opener/_registry.py new file mode 100644 index 00000000..9dcbf74d --- /dev/null +++ b/fs/opener/_registry.py @@ -0,0 +1,253 @@ +# coding: utf-8 +""" +fs.opener._registry +=================== + +Defines the Registry, which maps protocols and FS URLs to their +respective Opener. +""" + +import re +import contextlib +import collections + +from ._base import Opener +from ._errors import OpenerError, ParseError, Unsupported + + +class Registry(object): + """ + A registry for `Opener` instances. + + """ + + ParseResult = collections.namedtuple( + 'ParseResult', + [ + 'protocol', + 'username', + 'password', + 'resource', + 'path' + ] + ) + + _RE_FS_URL = re.compile(r''' + ^ + (.*?) + :\/\/ + + (?: + (?:(.*?)@(.*?)) + |(.*?) + ) + + (?: + !(.*?)$ + )*$ + ''', re.VERBOSE) + + @classmethod + def parse(cls, fs_url): + """ + Parse a Filesystem URL and return a :class:`ParseResult`, or raise + :class:`ParseError` (subclass of ValueError) if the FS URL is + not value. + + :param fs_url: A filesystem URL + :type fs_url: str + :rtype: :class:`ParseResult` + + """ + match = cls._RE_FS_URL.match(fs_url) + if match is None: + raise ParseError('{!r} is not a fs2 url'.format(fs_url)) + + fs_name, credentials, url1, url2, path = match.groups() + if credentials: + username, _, password = credentials.partition(':') + url = url1 + else: + username = None + password = None + url = url2 + return cls.ParseResult( + fs_name, + username, + password, + url, + path + ) + + def __init__(self, default_opener='osfs'): + """ + Create a registry object. + + :param default_opener: The protocol to use, if one is not + supplied. The default is to use 'osfs', so that the FS URL + is treated as a system path if no protocol is given. + + """ + self.default_opener = default_opener + self.protocols = {} + + def install(self, opener): + """ + Install an opener. + + :param opener: An :class:`Opener` instance, or a callable + that returns an opener instance. + + May be used as a class decorator. For example:: + + registry = Registry() + + @registry.install + class ArchiveOpener(Opener): + protocols = ['zip', 'tar'] + + """ + if not isinstance(opener, Opener): + opener = opener() + assert opener.protocols, "must list one or more protocols" + for protocol in opener.protocols: + self.protocols[protocol] = opener + + def open(self, + fs_url, + writeable=True, + create=False, + cwd=".", + default_protocol='osfs'): + """ + Open a filesystem from a FS URL. Returns a tuple of a filesystem + object and a path. If there is no path in the FS URL, the path + value will be ``None``. + + :param str fs_url: A filesystem URL + :param bool writeable: True if the filesystem must be writeable. + :param bool create: True if the filesystem should be created if + it does not exist. + :param cwd: The current working directory. + :type cwd: str or None + :rtype: Tuple of ``(, )`` + + """ + + if '://' not in fs_url: + # URL may just be a path + fs_url = "{}://{}".format(default_protocol, fs_url) + + parse_result = self.parse(fs_url) + protocol = parse_result.protocol + open_path = parse_result.path + + opener = self.protocols.get(protocol, None) + + if not opener: + raise Unsupported( + "protocol '{}' is not supported".format(protocol) + ) + + open_fs = opener.open_fs( + fs_url, + parse_result, + writeable, + create, + cwd + ) + return open_fs, open_path + + def open_fs(self, + fs_url, + writeable=True, + create=False, + cwd=".", + default_protocol='osfs'): + """ + Open a filesystem object from a FS URL (ignoring the path + component). + + :param str fs_url: A filesystem URL + :param parse_result: A parsed filesystem URL. + :type parse_result: :class:`ParseResult` + :param bool writeable: True if the filesystem must be writeable. + :param bool create: True if the filesystem should be created if + it does not exist. + :param str cwd: The current working directory (generally only + relevant for OS filesystems). + :param str default_protocol: The protocol to use if one is not + supplied in the FS URL (defaults to ``"osfs"``). + :returns: :class:`~fs.base.FS` object + + """ + from ..base import FS + if isinstance(fs_url, FS): + _fs = fs_url + else: + _fs, _path = self.open( + fs_url, + writeable=writeable, + create=create, + cwd=cwd, + default_protocol=default_protocol + ) + return _fs + + @contextlib.contextmanager + def manage_fs(self, fs_url, create=False, writeable=True, cwd='.'): + ''' + A context manager opens / closes a filesystem. + + :param fs_url: A FS instance or a FS URL. + :type fs_url: str or FS + :param bool create: If ``True``, then create the filesytem if it + doesn't already exist. + :param bool writeable: If ``True``, then the filesystem should be + writeable. + :param str cwd: The current working directory, if opening a + :class:`~fs.osfs.OSFS`. + + Sometimes it is convenient to be able to pass either a FS object + *or* an FS URL to a function. This context manager handles the + required logic for that. + + Here's an example:: + + def print_ls(list_fs): + """List a directory.""" + with manage_fs(list_fs) as fs: + print(" ".join(fs.listdir())) + + This function may be used in two ways. You may either pass either a + ``str``, as follows:: + + print_list('zip://projects.zip') + + Or, an FS instance:: + + from fs.osfs import OSFS + projects_fs = OSFS('~/') + print_list(projects_fs) + + ''' + from ..base import FS + if isinstance(fs_url, FS): + yield fs_url + else: + _fs = self.open_fs( + fs_url, + create=create, + writeable=writeable, + cwd=cwd + ) + try: + yield _fs + except: + raise + finally: + _fs.close() + + + +registry = Registry() diff --git a/fs/opener/ftpfs.py b/fs/opener/ftpfs.py new file mode 100644 index 00000000..9e33b70e --- /dev/null +++ b/fs/opener/ftpfs.py @@ -0,0 +1,27 @@ +# coding: utf-8 +"""Defines the FTPOpener.""" + +from ._base import Opener +from ._registry import registry + +@registry.install +class FTPOpener(Opener): + protocols = ['ftp'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..ftpfs import FTPFS + ftp_host, _, dir_path = parse_result.resource.partition('/') + ftp_host, _, ftp_port = ftp_host.partition(':') + ftp_port = int(ftp_port) if ftp_port.isdigit() else 21 + ftp_fs = FTPFS( + ftp_host, + port=ftp_port, + user=parse_result.username, + passwd=parse_result.password, + ) + ftp_fs = ( + ftp_fs.opendir(dir_path) + if dir_path else + ftp_fs + ) + return ftp_fs diff --git a/fs/opener/memoryfs.py b/fs/opener/memoryfs.py new file mode 100644 index 00000000..1bb67d9d --- /dev/null +++ b/fs/opener/memoryfs.py @@ -0,0 +1,14 @@ +# coding: utf-8 +"""Defines the MemOpener.""" + +from ._base import Opener +from ._registry import registry + +@registry.install +class MemOpener(Opener): + protocols = ['mem'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..memoryfs import MemoryFS + mem_fs = MemoryFS() + return mem_fs diff --git a/fs/opener/osfs.py b/fs/opener/osfs.py new file mode 100644 index 00000000..b9bfc2ef --- /dev/null +++ b/fs/opener/osfs.py @@ -0,0 +1,15 @@ +"""Defines the OSFSOpener.""" +from ._base import Opener +from ._registry import registry + +@registry.install +class OSFSOpener(Opener): + protocols = ['file', 'osfs'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..osfs import OSFS + from os.path import abspath, normpath, join + _path = abspath(join(cwd, parse_result.resource)) + path = normpath(_path) + osfs = OSFS(path, create=create) + return osfs diff --git a/fs/opener/tarfs.py b/fs/opener/tarfs.py new file mode 100644 index 00000000..eddd5838 --- /dev/null +++ b/fs/opener/tarfs.py @@ -0,0 +1,17 @@ +# coding: utf-8 +"""Defines the TarOpener.""" + +from ._base import Opener +from ._registry import registry + +@registry.install +class TarOpener(Opener): + protocols = ['tar'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..tarfs import TarFS + tar_fs = TarFS( + parse_result.resource, + write=create + ) + return tar_fs diff --git a/fs/opener/tempfs.py b/fs/opener/tempfs.py new file mode 100644 index 00000000..f43d2155 --- /dev/null +++ b/fs/opener/tempfs.py @@ -0,0 +1,14 @@ +# coding: utf-8 +"""Defines the TempOpener.""" + +from ._base import Opener +from ._registry import registry + +@registry.install +class TempOpener(Opener): + protocols = ['temp'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..tempfs import TempFS + temp_fs = TempFS(identifier=parse_result.resource) + return temp_fs diff --git a/fs/opener/zipfs.py b/fs/opener/zipfs.py new file mode 100644 index 00000000..af6f624d --- /dev/null +++ b/fs/opener/zipfs.py @@ -0,0 +1,17 @@ +# coding: utf-8 +"""Defines the ZipOpener.""" + +from ._base import Opener +from ._registry import registry + +@registry.install +class ZipOpener(Opener): + protocols = ['zip'] + + def open_fs(self, fs_url, parse_result, writeable, create, cwd): + from ..zipfs import ZipFS + zip_fs = ZipFS( + parse_result.resource, + write=create + ) + return zip_fs diff --git a/tests/test_opener.py b/tests/test_opener.py index b573b3be..03791009 100644 --- a/tests/test_opener.py +++ b/tests/test_opener.py @@ -17,7 +17,7 @@ def test_parse_not_url(self): def test_parse_simple(self): parsed = opener.parse('osfs://foo/bar') - expected = opener.ParseResult( + expected = opener.registry.ParseResult( 'osfs', None, None, @@ -28,7 +28,7 @@ def test_parse_simple(self): def test_parse_credentials(self): parsed = opener.parse('ftp://user:pass@ftp.example.org') - expected = opener.ParseResult( + expected = opener.registry.ParseResult( 'ftp', 'user', 'pass', @@ -38,7 +38,7 @@ def test_parse_credentials(self): self.assertEqual(expected, parsed) parsed = opener.parse('ftp://user@ftp.example.org') - expected = opener.ParseResult( + expected = opener.registry.ParseResult( 'ftp', 'user', '', @@ -49,7 +49,7 @@ def test_parse_credentials(self): def test_parse_path(self): parsed = opener.parse('osfs://foo/bar!example.txt') - expected = opener.ParseResult( + expected = opener.registry.ParseResult( 'osfs', None, None,