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,