diff --git a/signac/__main__.py b/signac/__main__.py index 75d9321f4..844a11637 100644 --- a/signac/__main__.py +++ b/signac/__main__.py @@ -248,6 +248,11 @@ def find_with_filter(args): def main_project(args): """Handle project subcommand.""" + warnings.warn( + "The `project` command is deprecated as of version 1.8 and will be removed in " + "version 2.0.", + FutureWarning, + ) project = get_project() if args.access: fn = project.create_access_module() @@ -258,7 +263,7 @@ def main_project(args): print(json.dumps(doc)) return if args.workspace: - print(project.workspace()) + print(project.workspace) else: print(project) @@ -279,7 +284,14 @@ def main_job(args): if args.create: job.init() if args.workspace: - print(job.workspace()) + warnings.warn( + "The `-w/--workspace` parameter is deprecated as of version 1.8 and will be removed in " + "version 2.0. Use -p/--path instead", + FutureWarning, + ) + args.path = True + if args.path: + print(job.path) else: print(job) @@ -658,7 +670,7 @@ def _main_import_interactive(project, origin, args): project_id=project.get_id(), job_banner="", root_path=project.root_directory(), - workspace_path=project.workspace(), + workspace_path=project.workspace, size=len(project), origin=args.origin, ), @@ -1169,7 +1181,7 @@ def write_history_file(): project_id=project.id, job_banner=f"\nJob:\t\t{job.id}" if job is not None else "", root_path=project.root_directory(), - workspace_path=project.workspace(), + workspace_path=project.workspace, size=len(project), ), ) @@ -1244,6 +1256,12 @@ def main(): action="store_true", help="Print the job's workspace path instead of the job id.", ) + parser_job.add_argument( + "-p", + "--path", + action="store_true", + help="Print the job's path instead of the job id.", + ) parser_job.add_argument( "-c", "--create", diff --git a/signac/cite.py b/signac/cite.py index 5b4a4ceef..c3029dcbf 100644 --- a/signac/cite.py +++ b/signac/cite.py @@ -7,6 +7,11 @@ from .common.deprecation import deprecated from .version import __version__ +""" +THIS MODULE IS DEPRECATED! +""" + + ARXIV_BIBTEX = """@online{signac, author = {Carl S. Adorf and Paul M. Dodd and Sharon C. Glotzer}, title = {signac - A Simple Data Management Framework}, diff --git a/signac/contrib/import_export.py b/signac/contrib/import_export.py index e1f582ef3..c3ad7e68d 100644 --- a/signac/contrib/import_export.py +++ b/signac/contrib/import_export.py @@ -344,7 +344,7 @@ def _export_jobs(jobs, path, copytree): # path_function is checked for uniqueness inside _make_path_function # Determine export path for each job. - paths = {job.workspace(): path_function(job) for job in jobs} + paths = {job.path: path_function(job) for job in jobs} # Check leaf/node consistency _check_directory_structure_validity(paths.values()) @@ -758,14 +758,14 @@ def _crawl_directory_data_space(root, project, schema_function): """ # We compare paths to the 'realpath' of the project workspace to catch loops. - workspace_real_path = os.path.realpath(project.workspace()) + workspace_real_path = os.path.realpath(project.workspace) for path, dirs, _ in os.walk(root): sp = schema_function(path) if sp is not None: del dirs[:] # skip sub-directories job = project.open_job(sp) - dst = job.workspace() + dst = job.path if os.path.realpath(path) == os.path.realpath(dst): continue # skip (already part of the data space) elif os.path.realpath(path).startswith(workspace_real_path): @@ -792,7 +792,7 @@ def _copy_to_job_workspace(src, job, copytree): Destination filename. """ - dst = job.workspace() + dst = job.path try: copytree(src, dst) except OSError as error: @@ -913,7 +913,7 @@ def __call__(self, copytree=None): _mkdir_p(os.path.dirname(fn_dst)) with open(fn_dst, "wb") as dst: dst.write(self.zipfile.read(name)) - return self.job.workspace() + return self.job.path def __str__(self): return f"{type(self).__name__}({self.root} -> {self.job})" @@ -998,7 +998,7 @@ def read_sp_manifest_file(path): sp = schema_function(name) if sp is not None: job = project.open_job(sp) - if os.path.exists(job.workspace()): + if os.path.exists(job.path): raise DestinationExistsError(job) mappings[name] = job skip_subdirs.add(name) @@ -1145,7 +1145,7 @@ def read_sp_manifest_file(path): sp = schema_function(name) if sp is not None: job = project.open_job(sp) - if os.path.exists(job.workspace()): + if os.path.exists(job.path): raise DestinationExistsError(job) mappings[name] = job skip_subdirs.add(name) diff --git a/signac/contrib/indexing.py b/signac/contrib/indexing.py index 98747d3c8..b6f787d8e 100644 --- a/signac/contrib/indexing.py +++ b/signac/contrib/indexing.py @@ -427,7 +427,7 @@ class SignacProjectCrawler(RegexFileCrawler): def __init__(self, root): from .project import get_project - root = get_project(root=root).workspace() + root = get_project(root=root).workspace self._statepoints = {} return super().__init__(root=root) diff --git a/signac/contrib/job.py b/signac/contrib/job.py index 1b172fb1f..6d815a73c 100644 --- a/signac/contrib/job.py +++ b/signac/contrib/job.py @@ -102,8 +102,8 @@ def _save(self): # Move the state point to an intermediate location as a backup. os.replace(self.filename, tmp_statepoint_file) try: - new_workspace = os.path.join(job._project.workspace(), new_id) - os.replace(job.workspace(), new_workspace) + new_workspace = os.path.join(job._project.workspace, new_id) + os.replace(job.path, new_workspace) except OSError as error: os.replace(tmp_statepoint_file, self.filename) # rollback if error.errno in (errno.EEXIST, errno.ENOTEMPTY, errno.EACCES): @@ -281,7 +281,7 @@ def __init__(self, project, statepoint=None, _id=None): def _initialize_lazy_properties(self): """Initialize all properties that are designed to be loaded lazily.""" with self._lock: - self._wd = None + self._path = None self._document = None self._stores = None self._cwd = [] @@ -321,9 +321,9 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented - return self.id == other.id and os.path.realpath( - self.workspace() - ) == os.path.realpath(other.workspace()) + return self.id == other.id and os.path.realpath(self.path) == os.path.realpath( + other.path + ) def __str__(self): """Return the job's id.""" @@ -334,35 +334,54 @@ def __repr__(self): self.__class__.__name__, repr(self._project), self.statepoint ) + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use Job.path instead.", + ) def workspace(self): - """Return the job's unique workspace directory. - - See :ref:`signac job -w ` for the command line equivalent. - - Returns - ------- - str - The path to the job's workspace directory. - - """ - if self._wd is None: - # We can rely on the project workspace to be well-formed, so just - # use str.join with os.sep instead of os.path.join for speed. - self._wd = os.sep.join((self._project.workspace(), self.id)) - return self._wd + """Alias for :attr:`~Job.path`.""" + return self.path @property def _statepoint_filename(self): """Get the path of the state point file for this job.""" # We can rely on the job workspace to be well-formed, so just # use str.join with os.sep instead of os.path.join for speed. - return os.sep.join((self.workspace(), self.FN_MANIFEST)) + return os.sep.join((self.path, self.FN_MANIFEST)) - @property + # Tell mypy to ignore type checking of the decorator because decorated + # properties aren't supported: https://github.com/python/mypy/issues/1362 + @property # type: ignore + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use Job.path instead.", + ) def ws(self): - """Alias for :meth:`~Job.workspace`.""" - return self.workspace() + """Alias for :attr:`~Job.path`.""" + return self.path + + @property + def path(self): + """str: The path to the job directory. + See :ref:`signac job -w ` for the command line equivalent. + """ + if self._path is None: + # We can rely on the project workspace to be well-formed, so just + # use str.join with os.sep instead of os.path.join for speed. + self._path = os.sep.join((self._project.workspace, self.id)) + return self._path + + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use job.statepoint = new_statepoint instead.", + ) def reset_statepoint(self, new_statepoint): """Overwrite the state point of this job while preserving job data. @@ -447,7 +466,13 @@ def update_statepoint(self, update, overwrite=False): @property def statepoint(self): - """Get the job's state point. + """Get or set the job's state point. + + Setting the state point to a different value will change the job id. + + For more information, see + `Modifying the State Point + `_. .. warning:: @@ -462,11 +487,16 @@ def statepoint(self): See :ref:`signac statepoint ` for the command line equivalent. + .. danger:: + + Use this function with caution! Resetting a job's state point + may sometimes be necessary, but can possibly lead to incoherent + data spaces. + Returns ------- dict Returns the job's state point. - """ with self._lock: if self._statepoint_requires_init: @@ -490,7 +520,6 @@ def statepoint(self, new_statepoint): ---------- new_statepoint : dict The new state point to be assigned. - """ self.reset_statepoint(new_statepoint) @@ -529,7 +558,7 @@ def document(self): with self._lock: if self._document is None: self.init() - fn_doc = os.path.join(self.workspace(), self.FN_DOCUMENT) + fn_doc = os.path.join(self.path, self.FN_DOCUMENT) self._document = BufferedJSONAttrDict( filename=fn_doc, write_concern=True ) @@ -610,7 +639,7 @@ def stores(self): with self._lock: if self._stores is None: self.init() - self._stores = H5StoreManager(self.workspace()) + self._stores = H5StoreManager(self.path) return self.init()._stores @property @@ -686,7 +715,7 @@ def init(self, force=False): # Create the workspace directory if it does not exist. try: - _mkdir_p(self.workspace()) + _mkdir_p(self.path) except OSError: logger.error( "Error occurred while trying to create " @@ -719,10 +748,10 @@ def clear(self): """ try: - for fn in os.listdir(self.workspace()): + for fn in os.listdir(self.path): if fn in (self.FN_MANIFEST, self.FN_DOCUMENT): continue - path = os.path.join(self.workspace(), fn) + path = os.path.join(self.path, fn) if os.path.isfile(path): os.remove(path) elif os.path.isdir(path): @@ -753,7 +782,7 @@ def remove(self): """ with self._lock: try: - shutil.rmtree(self.workspace()) + shutil.rmtree(self.path) except OSError as error: if error.errno != errno.ENOENT: raise @@ -784,9 +813,9 @@ def move(self, project): with self._lock: statepoint = self.statepoint() dst = project.open_job(statepoint) - _mkdir_p(project.workspace()) + _mkdir_p(project.workspace) try: - os.replace(self.workspace(), dst.workspace()) + os.replace(self.path, dst.path) except OSError as error: if error.errno == errno.ENOENT: raise RuntimeError( @@ -872,7 +901,7 @@ def fn(self, filename): The full workspace path of the file. """ - return os.path.join(self.workspace(), filename) + return os.path.join(self.path, filename) def isfile(self, filename): """Return True if file exists in the job's workspace. @@ -906,8 +935,8 @@ def open(self): """ self._cwd.append(os.getcwd()) self.init() - logger.info(f"Enter workspace '{self.workspace()}'.") - os.chdir(self.workspace()) + logger.info(f"Enter workspace '{self.path}'.") + os.chdir(self.path) def close(self): """Close the job and switch to the previous working directory.""" diff --git a/signac/contrib/linked_view.py b/signac/contrib/linked_view.py index a30bd5d95..aa000da53 100644 --- a/signac/contrib/linked_view.py +++ b/signac/contrib/linked_view.py @@ -103,10 +103,10 @@ def create_linked_view(project, prefix=None, job_ids=None, index=None, path=None links = {} for job in jobs: paths = os.path.join(path_function(job), "job") - links[paths] = job.workspace() + links[paths] = job.path if not links: # data space contains less than two elements for job in project.find_jobs(): - links["./job"] = job.workspace() + links["./job"] = job.path assert len(links) < 2 _check_directory_structure_validity(links.keys()) _update_view(prefix, links) diff --git a/signac/contrib/project.py b/signac/contrib/project.py index c50ee314c..7802a40b9 100644 --- a/signac/contrib/project.py +++ b/signac/contrib/project.py @@ -41,7 +41,7 @@ from .hashing import calc_id from .indexing import MainCrawler, SignacProjectCrawler from .job import Job -from .schema import ProjectSchema +from .schema import ProjectSchema, _collect_by_type from .utility import _mkdir_p, _nested_dicts_to_dotted_keys, split_and_print_progress logger = logging.getLogger(__name__) @@ -87,6 +87,19 @@ def get_indexes(root): _DEFAULT_PROJECT_NAME = None +class _CallableString(str): + # A string object that returns itself when called. Introduced temporarily + # to support deprecating Project.workspace, which will become a property. + def __call__(self): + assert version.parse(__version__) < version.parse("2.0.0") + warnings.warn( + "This method will soon become a property and should be used " + "without the call operator (parentheses).", + FutureWarning, + ) + return self + + class JobSearchIndex: """Search for specific jobs with filters. @@ -216,7 +229,7 @@ def __setitem__(self, key, value): def _invalidate_config_cache(project): """Invalidate cached properties derived from a project config.""" project._id = None - project._rd = None + project._path = None project._wd = None @@ -263,9 +276,7 @@ def __init__(self, config=None): self._lock = RLock() # Prepare cached properties derived from the project config. - self._id = None - self._rd = None - self._wd = None + _invalidate_config_cache(self) # Ensure that the project id is configured. if self.id is None: @@ -286,9 +297,9 @@ def __init__(self, config=None): self._stores = None # Prepare Workspace Directory - if not os.path.isdir(self.workspace()): + if not os.path.isdir(self.workspace): try: - _mkdir_p(self.workspace()) + _mkdir_p(self.workspace) except OSError: logger.error( "Error occurred while trying to create " @@ -332,7 +343,7 @@ def _repr_html_(self): "

" + f"Project: {self.id}
" + f"Root: {self.root_directory()}
" - + f"Workspace: {self.workspace()}
" + + f"Workspace: {self.workspace}
" + f"Size: {len(self)}" + "

" + self.find_jobs()._repr_html_jobs() @@ -353,47 +364,35 @@ def config(self): """ return self._config + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use Project.path instead.", + ) def root_directory(self): - """Return the project's root directory. - - Returns - ------- - str - Path of project directory. + """Alias for :attr:`~Project.path`.""" + return self.path - """ - if self._rd is None: - self._rd = self.config["project_dir"] - return self._rd + @property + def path(self): + """str: The path to the project directory.""" + if self._path is None: + self._path = self.config["project_dir"] + return self._path + @property def workspace(self): - """Return the project's workspace directory. - - The workspace defaults to `project_root/workspace`. - Configure this directory with the 'workspace_dir' - attribute. - If the specified directory is a relative path, - the absolute path is relative from the project's - root directory. - - .. note:: - The configuration will respect environment variables, - such as ``$HOME``. + """str: The project's workspace directory. See :ref:`signac project -w ` for the command line equivalent. - - Returns - ------- - str - Path of workspace directory. - """ if self._wd is None: wd = os.path.expandvars(self.config.get("workspace_dir", "workspace")) self._wd = ( wd if os.path.isabs(wd) else os.path.join(self.root_directory(), wd) ) - return self._wd + return _CallableString(self._wd) @deprecated( deprecated_in="1.3", @@ -517,18 +516,6 @@ def isfile(self, filename): """ return os.path.isfile(self.fn(filename)) - def _reset_document(self, new_doc): - """Reset document to new document passed. - - Parameters - ---------- - new_doc : dict - The new project document. - - """ - with self._lock: - self.document.reset(new_doc) - @property def document(self): """Get document associated with this project. @@ -557,7 +544,8 @@ def document(self, new_doc): The new project document. """ - self._reset_document(new_doc) + with self._lock: + self.document.reset(new_doc) @property def doc(self): @@ -731,32 +719,36 @@ def _job_dirs(self): """ try: - for d in os.listdir(self.workspace()): + for d in os.listdir(self.workspace): if JOB_ID_REGEX.match(d): yield d except OSError as error: if error.errno == errno.ENOENT: - if os.path.islink(self.workspace()): + if os.path.islink(self.workspace): raise WorkspaceError( - f"The link '{self.workspace()}' pointing to the workspace is broken." + f"The link '{self.workspace}' pointing to the workspace is broken." ) - elif not os.path.isdir(os.path.dirname(self.workspace())): + elif not os.path.isdir(os.path.dirname(self.workspace)): logger.warning( "The path to the workspace directory " - "('{}') does not exist.".format( - os.path.dirname(self.workspace()) - ) + "('{}') does not exist.".format(os.path.dirname(self.workspace)) ) else: logger.info( - f"The workspace directory '{self.workspace()}' does not exist!" + f"The workspace directory '{self.workspace}' does not exist!" ) else: logger.error( - f"Unable to access the workspace directory '{self.workspace()}'." + f"Unable to access the workspace directory '{self.workspace}'." ) raise WorkspaceError(error) + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="The num_jobs method is deprecated. Use len(project) instead.", + ) def num_jobs(self): """Return the number of initialized jobs. @@ -766,6 +758,9 @@ def num_jobs(self): Count of initialized jobs. """ + return len(self) + + def __len__(self): # We simply count the the number of valid directories and avoid building a list # for improved performance. i = 0 @@ -773,8 +768,6 @@ def num_jobs(self): pass return i - __len__ = num_jobs - def _contains_job_id(self, job_id): """Determine whether a job id is in the project's data space. @@ -790,8 +783,8 @@ def _contains_job_id(self, job_id): """ # We can rely on the project workspace to be well-formed, so just use - # str.join with os.sep instead of os.path.join for speed. - return os.path.exists(os.sep.join((self.workspace(), job_id))) + # str.join with os.sep instead of os.path.join for performance. + return os.path.exists(os.sep.join((self.workspace, job_id))) def __contains__(self, job): """Determine whether a job is in the project's data space. @@ -926,7 +919,9 @@ def detect_schema(self, exclude_const=False, subset=None, index=None): statepoint_index = _build_job_statepoint_index( exclude_const=exclude_const, index=index ) - return ProjectSchema.detect(statepoint_index) + return ProjectSchema( + {key: _collect_by_type(value) for key, value in statepoint_index} + ) @deprecated( deprecated_in="1.3", @@ -1344,12 +1339,12 @@ def _get_statepoint_from_workspace(self, job_id): """ # We can rely on the project workspace to be well-formed, so just use # str.join with os.sep instead of os.path.join for speed. - fn_manifest = os.sep.join((self.workspace(), job_id, self.Job.FN_MANIFEST)) + fn_manifest = os.sep.join((self.workspace, job_id, self.Job.FN_MANIFEST)) try: with open(fn_manifest, "rb") as manifest: return json.loads(manifest.read().decode()) except (OSError, ValueError) as error: - if os.path.isdir(os.sep.join((self.workspace(), job_id))): + if os.path.isdir(os.sep.join((self.workspace, job_id))): logger.error( "Error while trying to access state " "point manifest file of job '{}': '{}'.".format(job_id, error) @@ -1530,7 +1525,7 @@ def create_linked_view(self, prefix=None, job_ids=None, index=None, path=None): deprecated_in="1.3", removed_in="2.0", current_version=__version__, - details="Use job.reset_statepoint() instead.", + details="Use job.statepoint = new_statepoint instead.", ) def reset_statepoint(self, job, new_statepoint): """Overwrite the state point of this job while preserving job data. @@ -1633,7 +1628,7 @@ def clone(self, job, copytree=shutil.copytree): """ dst = self.open_job(job.statepoint()) try: - copytree(job.workspace(), dst.workspace()) + copytree(job.path, dst.path) except OSError as error: if error.errno == errno.EEXIST: raise DestinationExistsError(dst) @@ -1935,8 +1930,8 @@ def repair(self, fn_statepoints=None, index=None, job_ids=None): "The job id of job '{}' is incorrect; " "it should be '{}'.".format(job_id, correct_id) ) - invalid_wd = os.path.join(self.workspace(), job_id) - correct_wd = os.path.join(self.workspace(), correct_id) + invalid_wd = os.path.join(self.workspace, job_id) + correct_wd = os.path.join(self.workspace, correct_id) try: os.replace(invalid_wd, correct_wd) except OSError as error: @@ -2002,7 +1997,7 @@ def _build_index(self, include_job_document=False): False). """ - wd = self.workspace() if self.Job is Job else None + wd = self.workspace if self.Job is Job else None for _id in self._find_job_ids(): doc = dict(_id=_id, sp=self._get_statepoint(_id)) if include_job_document: @@ -2157,7 +2152,7 @@ def index( """ if formats is None: - root = self.workspace() + root = self.workspace def _full_doc(doc): """Add `signac_id` and `root` to the index document. @@ -2276,8 +2271,8 @@ def temporary_project(self, name=None, dir=None): if name is None: name = os.path.join(self.id, str(uuid.uuid4())) if dir is None: - dir = self.workspace() - _mkdir_p(self.workspace()) # ensure workspace exists + dir = self.workspace + _mkdir_p(self.workspace) # ensure workspace exists with TemporaryProject(name=name, cls=type(self), dir=dir) as tmp_project: yield tmp_project @@ -2350,7 +2345,7 @@ def init_project(cls, name=None, root=None, workspace=None, make_dir=True): assert project.id == str(name) if workspace is not None: assert os.path.realpath(workspace) == os.path.realpath( - project.workspace() + project.workspace ) return project except AssertionError: @@ -2638,17 +2633,19 @@ def __iter__(self): self._project, self._project._find_job_ids(self._filter) ) + @deprecated( + deprecated_in="0.9.6", + removed_in="2.0", + current_version=__version__, + details="Use next(iter(...)) instead.", + ) def next(self): """Return the next element. - This function is deprecated. Users should use ``next(iter(..))`` instead. + This function is deprecated. Users should use ``next(iter(...))`` instead. .. deprecated:: 0.9.6 """ - warnings.warn( - "Calling next() directly on a JobsCursor is deprecated! Use next(iter(..)) instead.", - FutureWarning, - ) if self._next_iter is None: self._next_iter = iter(self) try: diff --git a/signac/contrib/schema.py b/signac/contrib/schema.py index 2502bab29..cdf9273c7 100644 --- a/signac/contrib/schema.py +++ b/signac/contrib/schema.py @@ -4,11 +4,17 @@ """Project Schema.""" import itertools +import warnings from collections import defaultdict as ddict from collections.abc import Mapping from numbers import Number from pprint import pformat +from packaging import version + +from ..common.deprecation import deprecated +from ..version import __version__ + class _Vividict(dict): """A dict that returns an empty _Vividict for keys that are missing. @@ -36,6 +42,8 @@ def _collect_by_type(values): A mapping of types to a set of input values of that type. """ + # TODO: This function should be moved to project.py in version 2.0. + assert version.parse(__version__) < version.parse("2.0.0") values_by_type = ddict(set) for v in values: values_by_type[type(v)].add(v) @@ -103,7 +111,7 @@ def remove_dict_placeholder(x): yield statepoint_key, statepoint_values -class ProjectSchema: +class ProjectSchema(Mapping): """A description of a project's state point schema. Parameters @@ -118,6 +126,11 @@ def __init__(self, schema=None): schema = {} self._schema = schema + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + ) @classmethod def detect(cls, statepoint_index): """Detect Project's state point schema. @@ -267,43 +280,36 @@ def _repr_html_(self): output += "
" + str(self) + "
" return output + # TODO: This method can be removed in signac 2.0 once support for list keys + # is removed. def __contains__(self, key_or_keys): - if isinstance(key_or_keys, str): - return key_or_keys in self._schema - key_or_keys = ".".join(key_or_keys) + # NotOverride default __contains__ to support sequence and str inputs. + assert version.parse(__version__) < version.parse("2.0.0") + if not isinstance(key_or_keys, str): + warnings.warn( + "Support for checking nested keys in a schema using a list of keys is deprecated " + "and will be removed in signac 2.0. Construct the nested key using '.'.join(keys) " + "instead.", + FutureWarning, + ) + key_or_keys = ".".join(key_or_keys) return key_or_keys in self._schema def __getitem__(self, key_or_keys): - if isinstance(key_or_keys, str): - return self._schema[key_or_keys] - return self._schema[".".join(key_or_keys)] + assert version.parse(__version__) < version.parse("2.0.0") + if not isinstance(key_or_keys, str): + warnings.warn( + "Support for checking nested keys in a schema using a list of keys is deprecated " + "and will be removed in signac 2.0. Construct the nested key using '.'.join(keys) " + "instead.", + FutureWarning, + ) + key_or_keys = ".".join(key_or_keys) + return self._schema[key_or_keys] def __iter__(self): return iter(self._schema) - def keys(self): - """Return schema keys.""" - return self._schema.keys() - - def values(self): - """Return schema values.""" - return self._schema.values() - - def items(self): - """Return schema items.""" - return self._schema.items() - - def __eq__(self, other): - """Check if two schemas are the same. - - Returns - ------- - bool - True if both schemas have the same keys and values. - - """ - return self._schema == other._schema - def difference(self, other, ignore_values=False): """Determine the difference between this and another project schema. @@ -332,6 +338,11 @@ def difference(self, other, ignore_values=False): ) return ret + @deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + ) def __call__(self, jobs_or_statepoints): """Evaluate the schema for the given state points. diff --git a/signac/core/utility.py b/signac/core/utility.py index 3afd88359..7f2c821b3 100644 --- a/signac/core/utility.py +++ b/signac/core/utility.py @@ -9,6 +9,10 @@ from ..common.deprecation import deprecated from ..version import __version__ +""" +THIS MODULE IS DEPRECATED! +""" + @deprecated( deprecated_in="1.3", @@ -40,6 +44,12 @@ def get_subject_from_certificate(fn_certificate): # noqa: D103, E261 return lines[0][len("subject=") :].strip() +@deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use packaging.version.parse instead.", +) class Version(dict): """Utility class to manage revision control numbers.""" @@ -77,6 +87,12 @@ def __repr__(self): return "Version({})".format(",".join((f"{k}={v}" for k, v in self.items()))) +@deprecated( + deprecated_in="1.8", + removed_in="2.0", + current_version=__version__, + details="Use packaging.version.Version instead.", +) def parse_version(version_str): """Parse a version number into a version object.""" p = re.compile( diff --git a/signac/sync.py b/signac/sync.py index 880f42057..0c6e0d7dd 100644 --- a/signac/sync.py +++ b/signac/sync.py @@ -217,8 +217,8 @@ def _sync_job_workspaces( if exclude and any([re.match(p, fn) for p in exclude]): logger.debug(f"File named '{fn}' is skipped (excluded).") continue - fn_src = os.path.join(src.workspace(), subdir, fn) - fn_dst = os.path.join(dst.workspace(), subdir, fn) + fn_src = os.path.join(src.path, subdir, fn) + fn_dst = os.path.join(dst.path, subdir, fn) if os.path.isfile(fn_src): copy(fn_src, fn_dst) elif recursive: @@ -232,8 +232,8 @@ def _sync_job_workspaces( if strategy is None: raise FileSyncConflict(fn) else: - fn_src = os.path.join(src.workspace(), subdir, fn) - fn_dst = os.path.join(dst.workspace(), subdir, fn) + fn_src = os.path.join(src.path, subdir, fn) + fn_dst = os.path.join(dst.path, subdir, fn) if strategy(src, dst, os.path.join(subdir, fn)): copy(fn_src, fn_dst) else: @@ -335,7 +335,7 @@ def sync_jobs( """ # Check identity - if _identical_path(src.workspace(), dst.workspace()): + if _identical_path(src.path, dst.path): raise ValueError("Source and destination can't be the same!") # check src and dst compatiblity @@ -363,7 +363,7 @@ def sync_jobs( proxy = dry_run else: proxy = _FileModifyProxy( - root=src.workspace(), + root=src.path, follow_symlinks=follow_symlinks, permissions=preserve_permissions, times=preserve_times, @@ -376,7 +376,7 @@ def sync_jobs( else: logger.debug(f"Synchronizing job '{src}'...") - if os.path.isdir(src.workspace()): + if os.path.isdir(src.path): if not dry_run: dst.init() _sync_job_workspaces( @@ -500,7 +500,7 @@ def sync_projects( # Setup data modification proxy proxy = _FileModifyProxy( - root=source.workspace(), + root=source.workspace, follow_symlinks=follow_symlinks, permissions=preserve_permissions, times=preserve_times, diff --git a/signac/testing.py b/signac/testing.py index 845ff1300..a899c5280 100644 --- a/signac/testing.py +++ b/signac/testing.py @@ -5,6 +5,8 @@ from itertools import cycle +# TODO: This feels like something that should be a test fixture or +# fixture-adjacent, not code that belongs in the main package. def init_jobs(project, nested=False, listed=False, heterogeneous=False): """Initialize a dataspace for testing purposes. diff --git a/tests/test_job.py b/tests/test_job.py index 2f223cbf3..5d579f668 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -132,14 +132,14 @@ class NonJob: def __init__(self, job): self.id = job.id - self._workspace = job.workspace() + self._workspace = job.path class JobSubclass(Job): """Minimal subclass that can be compared with Job objects.""" def __init__(self, job): self._id = job.id - self._workspace = job.workspace() + self._path = job.path def workspace(self): return self._workspace @@ -159,7 +159,7 @@ def workspace(self): def test_isfile(self): job = self.project.open_job({"a": 0}) fn = "test.txt" - fn_ = os.path.join(job.workspace(), fn) + fn_ = os.path.join(job.path, fn) assert not job.isfile(fn) job.init() assert not job.isfile(fn) @@ -526,21 +526,17 @@ def test_str(self): class TestJobOpenAndClosing(TestJobBase): def test_init(self): job = self.open_job(test_token) - assert not os.path.isdir(job.workspace()) + assert not os.path.isdir(job.path) job.init() - assert job.workspace() == job.ws - assert os.path.isdir(job.workspace()) - assert os.path.isdir(job.ws) - assert os.path.exists(os.path.join(job.workspace(), job.FN_MANIFEST)) + assert os.path.isdir(job.path) + assert os.path.exists(os.path.join(job.path, job.FN_MANIFEST)) def test_chained_init(self): job = self.open_job(test_token) - assert not os.path.isdir(job.workspace()) + assert not os.path.isdir(job.path) job = self.open_job(test_token).init() - assert job.workspace() == job.ws - assert os.path.isdir(job.workspace()) - assert os.path.isdir(job.ws) - assert os.path.exists(os.path.join(job.workspace(), job.FN_MANIFEST)) + assert os.path.isdir(job.path) + assert os.path.exists(os.path.join(job.path, job.FN_MANIFEST)) def test_construction(self): from signac import Project # noqa: F401 @@ -600,37 +596,37 @@ def test_open_job_recursive(self): cwd = rp(os.getcwd()) job = self.open_job(test_token) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) assert cwd == rp(os.getcwd()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) os.chdir(self.project.root_directory()) assert cwd == rp(os.getcwd()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) assert cwd == rp(os.getcwd()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) os.chdir(self.project.root_directory()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) assert rp(os.getcwd()) == rp(self.project.root_directory()) assert cwd == rp(os.getcwd()) with job: job.close() assert cwd == rp(os.getcwd()) with job: - assert rp(job.workspace()) == rp(os.getcwd()) + assert rp(job.path) == rp(os.getcwd()) assert cwd == rp(os.getcwd()) assert cwd == rp(os.getcwd()) def test_corrupt_workspace(self): job = self.open_job(test_token) job.init() - fn_manifest = os.path.join(job.workspace(), job.FN_MANIFEST) + fn_manifest = os.path.join(job.path, job.FN_MANIFEST) with open(fn_manifest, "w") as file: file.write("corrupted") job2 = self.open_job(test_token) @@ -890,7 +886,7 @@ def test_remove(self): job.document[key] = d assert key in job.document assert len(job.document) == 1 - fn_test = os.path.join(job.workspace(), "test") + fn_test = os.path.join(job.path, "test") with open(fn_test, "w") as file: file.write("test") assert os.path.isfile(fn_test) @@ -1420,7 +1416,7 @@ def test_remove(self): job.data[key] = d assert key in job.data assert len(job.data) == 1 - fn_test = os.path.join(job.workspace(), "test") + fn_test = os.path.join(job.path, "test") with open(fn_test, "w") as file: file.write("test") assert os.path.isfile(fn_test) @@ -1906,7 +1902,7 @@ def test_remove(self): job.stores.test[key] = d assert key in job.stores.test assert len(job.stores.test) == 1 - fn_test = os.path.join(job.workspace(), "test") + fn_test = os.path.join(job.path, "test") with open(fn_test, "w") as file: file.write("test") assert os.path.isfile(fn_test) diff --git a/tests/test_project.py b/tests/test_project.py index ada7c711e..0787ba355 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -109,7 +109,12 @@ def test_root_directory(self): assert self._tmp_pr == self.project.root_directory() def test_workspace_directory(self): - assert self._tmp_wd == self.project.workspace() + assert self._tmp_wd == self.project.workspace + + def test_workspace_property(self): + with pytest.warns(FutureWarning): + ws = self.project.workspace() + assert ws == self.project.workspace def test_config_modification(self): # In-memory modification of the project configuration is @@ -121,10 +126,10 @@ def test_config_modification(self): def test_workspace_directory_with_env_variable(self): os.environ["SIGNAC_ENV_DIR_TEST"] = self._tmp_wd self.project.config["workspace_dir"] = "${SIGNAC_ENV_DIR_TEST}" - assert self._tmp_wd == self.project.workspace() + assert self._tmp_wd == self.project.workspace def test_workspace_directory_exists(self): - assert os.path.exists(self.project.workspace()) + assert os.path.exists(self.project.workspace) def test_fn(self): assert self.project.fn("test/abc") == os.path.join( @@ -233,21 +238,21 @@ def root_path(): # Returns 'C:\\' on Windows, '/' on other platforms return os.path.abspath(os.sep) - assert self.project.workspace() == norm_path(self._tmp_wd) + assert self.project.workspace == norm_path(self._tmp_wd) abs_path = os.path.join(root_path(), "path", "to", "workspace") self.project.config["workspace_dir"] = abs_path - assert self.project.workspace() == norm_path(abs_path) + assert self.project.workspace == norm_path(abs_path) rel_path = norm_path(os.path.join("path", "to", "workspace")) self.project.config["workspace_dir"] = rel_path - assert self.project.workspace() == norm_path( - os.path.join(self.project.root_directory(), self.project.workspace()) + assert self.project.workspace == norm_path( + os.path.join(self.project.root_directory(), self.project.workspace) ) def test_no_workspace_warn_on_find(self, caplog): - if os.path.exists(self.project.workspace()): - os.rmdir(self.project.workspace()) + if os.path.exists(self.project.workspace): + os.rmdir(self.project.workspace) with caplog.at_level(logging.INFO): list(self.project.find_jobs()) # Python < 3.8 will return 2 messages. @@ -258,7 +263,7 @@ def test_no_workspace_warn_on_find(self, caplog): @pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.") def test_workspace_broken_link_error_on_find(self): - wd = self.project.workspace() + wd = self.project.workspace os.symlink(wd + "~", self.project.fn("workspace-link")) self.project.config["workspace_dir"] = "workspace-link" with pytest.raises(WorkspaceError): @@ -267,13 +272,13 @@ def test_workspace_broken_link_error_on_find(self): def test_workspace_read_only_path(self): # Create file where workspace would be, thus preventing the creation # of the workspace directory. - if os.path.exists(self.project.workspace()): - os.rmdir(self.project.workspace()) - with open(os.path.join(self.project.workspace()), "w"): + if os.path.exists(self.project.workspace): + os.rmdir(self.project.workspace) + with open(os.path.join(self.project.workspace), "w"): pass with pytest.raises(OSError): # Ensure that the file is in place. - os.mkdir(self.project.workspace()) + os.mkdir(self.project.workspace) assert issubclass(WorkspaceError, OSError) @@ -285,7 +290,7 @@ def test_workspace_read_only_path(self): logging.disable(logging.NOTSET) assert not os.path.isdir(self._tmp_wd) - assert not os.path.isdir(self.project.workspace()) + assert not os.path.isdir(self.project.workspace) def test_find_job_ids(self): statepoints = [{"a": i} for i in range(5)] @@ -565,10 +570,10 @@ def test_rename_workspace(self): job = self.project.open_job(dict(a=0)) job.init() # First, we move the job to the wrong directory. - wd = job.workspace() - wd_invalid = os.path.join(self.project.workspace(), "0" * 32) + wd = job.path + wd_invalid = os.path.join(self.project.workspace, "0" * 32) os.replace(wd, wd_invalid) # Move to incorrect id. - assert not os.path.exists(job.workspace()) + assert not os.path.exists(job.path) try: logging.disable(logging.CRITICAL) @@ -840,16 +845,21 @@ def test_schema(self): assert len(s) == 9 for k in "const", "const2.const3", "a", "b.b2", "c", "d", "e.e2", "f.f2": assert k in s - assert k.split(".") in s + if "." in k: + with pytest.warns(FutureWarning): + assert k.split(".") in s # The following calls should not error out. s[k] - s[k.split(".")] + if "." in k: + with pytest.warns(FutureWarning): + s[k.split(".")] repr(s) assert s.format() == str(s) s = self.project.detect_schema(exclude_const=True) assert len(s) == 7 assert "const" not in s - assert ("const2", "const3") not in s + with pytest.warns(FutureWarning): + assert ("const2", "const3") not in s assert "const2.const3" not in s assert type not in s["e"] @@ -1359,7 +1369,7 @@ def test_export_import_tarfile(self): with TarFile(name=target) as tarfile: for i in range(10): assert f"a/{i}" in tarfile.getnames() - os.replace(self.project.workspace(), self.project.workspace() + "~") + os.replace(self.project.workspace, self.project.workspace + "~") assert len(self.project) == 0 self.project.import_from(origin=target) assert len(self.project) == 10 @@ -1395,7 +1405,7 @@ def test_export_import_tarfile_zipped(self): for i in range(10): assert f"a/{i}" in tarfile.getnames() assert f"a/{i}/sub-dir/signac_statepoint.json" in tarfile.getnames() - os.replace(self.project.workspace(), self.project.workspace() + "~") + os.replace(self.project.workspace, self.project.workspace + "~") assert len(self.project) == 0 self.project.import_from(origin=target) assert len(self.project) == 10 @@ -1420,7 +1430,7 @@ def test_export_import_zipfile(self): for i in range(10): assert f"a/{i}/signac_statepoint.json" in zipfile.namelist() assert f"a/{i}/sub-dir/signac_statepoint.json" in zipfile.namelist() - os.replace(self.project.workspace(), self.project.workspace() + "~") + os.replace(self.project.workspace, self.project.workspace + "~") assert len(self.project) == 0 self.project.import_from(origin=target) assert len(self.project) == 10 @@ -1473,7 +1483,7 @@ def test_export_import_conflict_synced_with_args(self): assert len(self.project.import_from(prefix_data)) == 10 selection = list(self.project.find_jobs(dict(a=0))) - os.replace(self.project.workspace(), self.project.workspace() + "~") + os.replace(self.project.workspace, self.project.workspace + "~") assert len(self.project) == 0 assert ( len(self.project.import_from(prefix_data, sync=dict(selection=selection))) @@ -1632,10 +1642,10 @@ def test_import_own_project(self): for i in range(10): self.project.open_job(dict(a=i)).init() ids_before_export = {job.id for job in self.project.find_jobs()} - self.project.import_from(origin=self.project.workspace()) + self.project.import_from(origin=self.project.workspace) assert ids_before_export == {job.id for job in self.project.find_jobs()} with self.project.temporary_project() as tmp_project: - tmp_project.import_from(origin=self.project.workspace()) + tmp_project.import_from(origin=self.project.workspace) assert ids_before_export == {job.id for job in self.project.find_jobs()} assert len(tmp_project) == len(self.project) @@ -1742,9 +1752,7 @@ def clean(filter=None): all_links, ) ) - src = set( - map(lambda j: os.path.realpath(j.workspace()), self.project.find_jobs()) - ) + src = set(map(lambda j: os.path.realpath(j.path), self.project.find_jobs())) assert len(all_links) == self.project.num_jobs() self.project.create_linked_view(prefix=view_prefix) all_links = list(_find_all_links(view_prefix)) @@ -1755,9 +1763,7 @@ def clean(filter=None): all_links, ) ) - src = set( - map(lambda j: os.path.realpath(j.workspace()), self.project.find_jobs()) - ) + src = set(map(lambda j: os.path.realpath(j.path), self.project.find_jobs())) assert src == dst # update with subset job_subset = self.project.find_jobs({"b": 0}) @@ -1778,7 +1784,7 @@ def clean(filter=None): all_links, ) ) - src = set(map(lambda j: os.path.realpath(j.workspace()), job_subset)) + src = set(map(lambda j: os.path.realpath(j.path), job_subset)) assert src == dst # some jobs removed clean({"b": 0}) @@ -1790,9 +1796,7 @@ def clean(filter=None): all_links, ) ) - src = set( - map(lambda j: os.path.realpath(j.workspace()), self.project.find_jobs()) - ) + src = set(map(lambda j: os.path.realpath(j.path), self.project.find_jobs())) assert src == dst # all jobs removed clean() @@ -1804,9 +1808,7 @@ def clean(filter=None): all_links, ) ) - src = set( - map(lambda j: os.path.realpath(j.workspace()), self.project.find_jobs()) - ) + src = set(map(lambda j: os.path.realpath(j.path), self.project.find_jobs())) assert src == dst @pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.") @@ -2285,19 +2287,19 @@ def test_get_project(self): signac.get_project(root=root) project = signac.init_project(name="testproject", root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root project = signac.Project.init_project(name="testproject", root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root project = signac.get_project(root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root project = signac.Project.get_project(root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root def test_get_project_all_printable_characters(self): @@ -2333,17 +2335,17 @@ def test_init(self): signac.get_project(root=root) project = signac.init_project(name="testproject", root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root # Second initialization should not make any difference. project = signac.init_project(name="testproject", root=root) project = signac.get_project(root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root project = signac.Project.get_project(root=root) assert project.id == "testproject" - assert project.workspace() == os.path.join(root, "workspace") + assert project.workspace == os.path.join(root, "workspace") assert project.root_directory() == root # Deviating initialization parameters should result in errors. with pytest.raises(RuntimeError): @@ -2405,7 +2407,7 @@ def test_get_job_invalid_workspace(self): # since no signac_statepoint.json exists. cwd = os.getcwd() try: - os.chdir(project.workspace()) + os.chdir(project.workspace) with pytest.raises(LookupError): project.get_job() with pytest.raises(LookupError): @@ -2468,8 +2470,8 @@ def test_get_job_symlink_other_project(self): job_a.init() job_b = project_b.open_job({"b": 1}) job_b.init() - symlink_path = os.path.join(project_b.workspace(), job_a._id) - os.symlink(job_a.ws, symlink_path) + symlink_path = os.path.join(project_b.workspace, job_a._id) + os.symlink(job_a.path, symlink_path) assert project_a.get_job(symlink_path) == job_a assert project_b.get_job(symlink_path) == job_a assert signac.get_job(symlink_path) == job_a diff --git a/tests/test_shell.py b/tests/test_shell.py index 9751bc060..4bb4fc138 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -218,7 +218,7 @@ def test_view_single(self): for sp in sps: assert os.path.isdir("view/job") assert os.path.realpath("view/job") == os.path.realpath( - project.open_job(sp).workspace() + project.open_job(sp).path ) @pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.") @@ -235,7 +235,7 @@ def test_view(self): assert os.path.isdir("view/a/{}/job".format(sp["a"])) assert os.path.realpath( "view/a/{}/job".format(sp["a"]) - ) == os.path.realpath(project.open_job(sp).workspace()) + ) == os.path.realpath(project.open_job(sp).path) @pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.") def test_view_incomplete_path_spec(self):