diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index 12cd0bdefe..1f2e1591b2 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -2,7 +2,7 @@ import os from avalon import api as avalon from pyblish import api as pyblish -# from avalon.tools import workfiles +from ..vendor import ftrack_api PARENT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.dirname(PARENT_DIR) diff --git a/pype/vendor/clique/__init__.py b/pype/vendor/clique/__init__.py deleted file mode 100644 index cdbd05a267..0000000000 --- a/pype/vendor/clique/__init__.py +++ /dev/null @@ -1,293 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -import re -from collections import defaultdict - -from ._version import __version__ -from .collection import Collection -from .error import CollectionError - - -#: Pattern for matching an index with optional padding. -DIGITS_PATTERN = '(?P(?P0*)\d+)' - -#: Common patterns that can be passed to :py:func:`~clique.assemble`. -PATTERNS = { - 'frames': '\.{0}\.\D+\d?$'.format(DIGITS_PATTERN), - 'versions': 'v{0}'.format(DIGITS_PATTERN) -} - - -def assemble( - iterable, patterns=None, minimum_items=2, case_sensitive=True, - assume_padded_when_ambiguous=False -): - '''Assemble items in *iterable* into discreet collections. - - *patterns* may be specified as a list of regular expressions to limit - the returned collection possibilities. Use this when interested in - collections that only match specific patterns. Each pattern must contain - the expression from :py:data:`DIGITS_PATTERN` exactly once. - - A selection of common expressions are available in :py:data:`PATTERNS`. - - .. note:: - - If a pattern is supplied as a string it will be automatically compiled - to a :py:class:`re.RegexObject` instance for convenience. - - When *patterns* is not specified, collections are formed by examining all - possible groupings of the items in *iterable* based around common numerical - components. - - *minimum_items* dictates the minimum number of items a collection must have - in order to be included in the result. The default is 2, filtering out - single item collections. - - If *case_sensitive* is False, then items will be treated as part of the same - collection when they only differ in casing. To avoid ambiguity, the - resulting collection will always be lowercase. For example, "item.0001.dpx" - and "Item.0002.dpx" would be part of the same collection, "item.%04d.dpx". - - .. note:: - - Any compiled *patterns* will also respect the set case sensitivity. - - For certain collections it may be ambiguous whether they are padded or not. - For example, 1000-1010 can be considered either an unpadded collection or a - four padded collection. By default, Clique is conservative and assumes that - the collection is unpadded. To change this behaviour, set - *assume_padded_when_ambiguous* to True and any ambiguous collection will have - a relevant padding set. - - .. note:: - - *assume_padded_when_ambiguous* has no effect on collections that are - unambiguous. For example, 1-100 will always be considered unpadded - regardless of the *assume_padded_when_ambiguous* setting. - - Return tuple of two lists (collections, remainder) where 'collections' is a - list of assembled :py:class:`~clique.collection.Collection` instances and - 'remainder' is a list of items that did not belong to any collection. - - ''' - collection_map = defaultdict(set) - collections = [] - remainder = [] - - # Compile patterns. - flags = 0 - if not case_sensitive: - flags |= re.IGNORECASE - - compiled_patterns = [] - - if patterns is not None: - if not patterns: - return collections, list(iterable) - - for pattern in patterns: - if isinstance(pattern, basestring): - compiled_patterns.append(re.compile(pattern, flags=flags)) - else: - compiled_patterns.append(pattern) - - else: - compiled_patterns.append(re.compile(DIGITS_PATTERN, flags=flags)) - - # Process iterable. - for item in iterable: - matched = False - - for pattern in compiled_patterns: - for match in pattern.finditer(item): - index = match.group('index') - - head = item[:match.start('index')] - tail = item[match.end('index'):] - - if not case_sensitive: - head = head.lower() - tail = tail.lower() - - padding = match.group('padding') - if padding: - padding = len(index) - else: - padding = 0 - - key = (head, tail, padding) - collection_map[key].add(int(index)) - matched = True - - if not matched: - remainder.append(item) - - # Form collections. - merge_candidates = [] - for (head, tail, padding), indexes in collection_map.items(): - collection = Collection(head, tail, padding, indexes) - collections.append(collection) - - if collection.padding == 0: - merge_candidates.append(collection) - - # Merge together collections that align on padding boundaries. For example, - # 0998-0999 and 1000-1001 can be merged into 0998-1001. Note that only - # indexes within the padding width limit are merged. If a collection is - # entirely merged into another then it will not be included as a separate - # collection in the results. - fully_merged = [] - for collection in collections: - if collection.padding == 0: - continue - - for candidate in merge_candidates: - if ( - candidate.head == collection.head and - candidate.tail == collection.tail - ): - merged_index_count = 0 - for index in candidate.indexes: - if len(str(abs(index))) == collection.padding: - collection.indexes.add(index) - merged_index_count += 1 - - if merged_index_count == len(candidate.indexes): - fully_merged.append(candidate) - - # Filter out fully merged collections. - collections = [collection for collection in collections - if collection not in fully_merged] - - # Filter out collections that do not have at least as many indexes as - # minimum_items. In addition, add any members of a filtered collection, - # which are not members of an unfiltered collection, to the remainder. - filtered = [] - remainder_candidates = [] - for collection in collections: - if len(collection.indexes) >= minimum_items: - filtered.append(collection) - else: - for member in collection: - remainder_candidates.append(member) - - for candidate in remainder_candidates: - # Check if candidate has already been added to remainder to avoid - # duplicate entries. - if candidate in remainder: - continue - - has_membership = False - - for collection in filtered: - if candidate in collection: - has_membership = True - break - - if not has_membership: - remainder.append(candidate) - - # Set padding for all ambiguous collections according to the - # assume_padded_when_ambiguous setting. - if assume_padded_when_ambiguous: - for collection in filtered: - if ( - not collection.padding and collection.indexes - ): - indexes = list(collection.indexes) - first_index_width = len(str(indexes[0])) - last_index_width = len(str(indexes[-1])) - if first_index_width == last_index_width: - collection.padding = first_index_width - - return filtered, remainder - - -def parse(value, pattern='{head}{padding}{tail} [{ranges}]'): - '''Parse *value* into a :py:class:`~clique.collection.Collection`. - - Use *pattern* to extract information from *value*. It may make use of the - following keys: - - * *head* - Common leading part of the collection. - * *tail* - Common trailing part of the collection. - * *padding* - Padding value in ``%0d`` format. - * *range* - Total range in the form ``start-end``. - * *ranges* - Comma separated ranges of indexes. - * *holes* - Comma separated ranges of missing indexes. - - .. note:: - - *holes* only makes sense if *range* or *ranges* is also present. - - ''' - # Construct regular expression for given pattern. - expressions = { - 'head': '(?P.*)', - 'tail': '(?P.*)', - 'padding': '%(?P\d*)d', - 'range': '(?P\d+-\d+)?', - 'ranges': '(?P[\d ,\-]+)?', - 'holes': '(?P[\d ,\-]+)' - } - - pattern_regex = re.escape(pattern) - for key, expression in expressions.items(): - pattern_regex = pattern_regex.replace( - '\{{{0}\}}'.format(key), - expression - ) - pattern_regex = '^{0}$'.format(pattern_regex) - - # Match pattern against value and use results to construct collection. - match = re.search(pattern_regex, value) - if match is None: - raise ValueError('Value did not match pattern.') - - groups = match.groupdict() - if 'padding' in groups and groups['padding']: - groups['padding'] = int(groups['padding']) - else: - groups['padding'] = 0 - - # Create collection and then add indexes. - collection = Collection( - groups.get('head', ''), - groups.get('tail', ''), - groups['padding'] - ) - - if groups.get('range', None) is not None: - start, end = map(int, groups['range'].split('-')) - collection.indexes.update(range(start, end + 1)) - - if groups.get('ranges', None) is not None: - parts = [part.strip() for part in groups['ranges'].split(',')] - for part in parts: - index_range = list(map(int, part.split('-', 2))) - - if len(index_range) > 1: - # Index range. - for index in range(index_range[0], index_range[1] + 1): - collection.indexes.add(index) - else: - # Single index. - collection.indexes.add(index_range[0]) - - if 'holes' in groups: - parts = [part.strip() for part in groups['holes'].split(',')] - for part in parts: - index_range = map(int, part.split('-', 2)) - - if len(index_range) > 1: - # Index range. - for index in range(index_range[0], index_range[1] + 1): - collection.indexes.remove(index) - else: - # Single index. - collection.indexes.remove(index_range[0]) - - return collection diff --git a/pype/vendor/clique/_version.py b/pype/vendor/clique/_version.py deleted file mode 100644 index 86d9c8b54c..0000000000 --- a/pype/vendor/clique/_version.py +++ /dev/null @@ -1,2 +0,0 @@ -__version__ = '1.5.0' - diff --git a/pype/vendor/clique/collection.py b/pype/vendor/clique/collection.py deleted file mode 100644 index 370ab7889e..0000000000 --- a/pype/vendor/clique/collection.py +++ /dev/null @@ -1,385 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -import re - -import descriptor -import error -import sorted_set - - -class Collection(object): - '''Represent group of items that differ only by numerical component.''' - - indexes = descriptor.Unsettable('indexes') - - def __init__(self, head, tail, padding, indexes=None): - '''Initialise collection. - - *head* is the leading common part whilst *tail* is the trailing - common part. - - *padding* specifies the "width" of the numerical component. An index - will be padded with zeros to fill this width. A *padding* of zero - implies no padding and width may be any size so long as no leading - zeros are present. - - *indexes* can specify a set of numerical indexes to initially populate - the collection with. - - .. note:: - - After instantiation, the ``indexes`` attribute cannot be set to a - new value using assignment:: - - >>> collection.indexes = [1, 2, 3] - AttributeError: Cannot set attribute defined as unsettable. - - Instead, manipulate it directly:: - - >>> collection.indexes.clear() - >>> collection.indexes.update([1, 2, 3]) - - ''' - super(Collection, self).__init__() - self.__dict__['indexes'] = sorted_set.SortedSet() - self._head = head - self._tail = tail - self.padding = padding - self._update_expression() - - if indexes is not None: - self.indexes.update(indexes) - - @property - def head(self): - '''Return common leading part.''' - return self._head - - @head.setter - def head(self, value): - '''Set common leading part to *value*.''' - self._head = value - self._update_expression() - - @property - def tail(self): - '''Return common trailing part.''' - return self._tail - - @tail.setter - def tail(self, value): - '''Set common trailing part to *value*.''' - self._tail = value - self._update_expression() - - def _update_expression(self): - '''Update internal expression.''' - self._expression = re.compile( - '^{0}(?P(?P0*)\d+?){1}$' - .format(re.escape(self.head), re.escape(self.tail)) - ) - - def __str__(self): - '''Return string represenation.''' - return self.format() - - def __repr__(self): - '''Return representation.''' - return '<{0} "{1}">'.format(self.__class__.__name__, self) - - def __iter__(self): - '''Return iterator over items in collection.''' - for index in self.indexes: - formatted_index = '{0:0{1}d}'.format(index, self.padding) - item = '{0}{1}{2}'.format(self.head, formatted_index, self.tail) - yield item - - def __contains__(self, item): - '''Return whether *item* is present in collection.''' - match = self.match(item) - if not match: - return False - - if not int(match.group('index')) in self.indexes: - return False - - return True - - def __eq__(self, other): - '''Return whether *other* collection is equal.''' - if not isinstance(other, Collection): - return NotImplemented - - return all([ - other.head == self.head, - other.tail == self.tail, - other.padding == self.padding, - other.indexes == self.indexes - ]) - - def __ne__(self, other): - '''Return whether *other* collection is not equal.''' - result = self.__eq__(other) - if result is NotImplemented: - return result - - return not result - - def __gt__(self, other): - '''Return whether *other* collection is greater than.''' - if not isinstance(other, Collection): - return NotImplemented - - a = (self.head, self.tail, self.padding, len(self.indexes)) - b = (other.head, other.tail, other.padding, len(other.indexes)) - - return a > b - - def __lt__(self, other): - '''Return whether *other* collection is less than.''' - result = self.__gt__(other) - if result is NotImplemented: - return result - - return not result - - def __ge__(self, other): - '''Return whether *other* collection is greater than or equal.''' - result = self.__eq__(other) - if result is NotImplemented: - return result - - if result is False: - result = self.__gt__(other) - - return result - - def __le__(self, other): - '''Return whether *other* collection is less than or equal.''' - result = self.__eq__(other) - if result is NotImplemented: - return result - - if result is False: - result = self.__lt__(other) - - return result - - def match(self, item): - '''Return whether *item* matches this collection expression. - - If a match is successful return data about the match otherwise return - None. - - ''' - match = self._expression.match(item) - if not match: - return None - - index = match.group('index') - padded = False - if match.group('padding'): - padded = True - - if self.padding == 0: - if padded: - return None - - elif len(index) != self.padding: - return None - - return match - - def add(self, item): - '''Add *item* to collection. - - raise :py:class:`~error.CollectionError` if *item* cannot be - added to the collection. - - ''' - match = self.match(item) - if match is None: - raise error.CollectionError( - 'Item does not match collection expression.' - ) - - self.indexes.add(int(match.group('index'))) - - def remove(self, item): - '''Remove *item* from collection. - - raise :py:class:`~error.CollectionError` if *item* cannot be - removed from the collection. - - ''' - match = self.match(item) - if match is None: - raise error.CollectionError( - 'Item not present in collection.' - ) - - index = int(match.group('index')) - try: - self.indexes.remove(index) - except KeyError: - raise error.CollectionError( - 'Item not present in collection.' - ) - - def format(self, pattern='{head}{padding}{tail} [{ranges}]'): - '''Return string representation as specified by *pattern*. - - Pattern can be any format accepted by Python's standard format function - and will receive the following keyword arguments as context: - - * *head* - Common leading part of the collection. - * *tail* - Common trailing part of the collection. - * *padding* - Padding value in ``%0d`` format. - * *range* - Total range in the form ``start-end`` - * *ranges* - Comma separated ranges of indexes. - * *holes* - Comma separated ranges of missing indexes. - - ''' - data = {} - data['head'] = self.head - data['tail'] = self.tail - - if self.padding: - data['padding'] = '%0{0}d'.format(self.padding) - else: - data['padding'] = '%d' - - if '{holes}' in pattern: - data['holes'] = self.holes().format('{ranges}') - - if '{range}' in pattern or '{ranges}' in pattern: - indexes = list(self.indexes) - indexes_count = len(indexes) - - if indexes_count == 0: - data['range'] = '' - - elif indexes_count == 1: - data['range'] = '{0}'.format(indexes[0]) - - else: - data['range'] = '{0}-{1}'.format( - indexes[0], indexes[-1] - ) - - if '{ranges}' in pattern: - separated = self.separate() - if len(separated) > 1: - ranges = [collection.format('{range}') - for collection in separated] - - else: - ranges = [data['range']] - - data['ranges'] = ', '.join(ranges) - - return pattern.format(**data) - - def is_contiguous(self): - '''Return whether entire collection is contiguous.''' - previous = None - for index in self.indexes: - if previous is None: - previous = index - continue - - if index != (previous + 1): - return False - - previous = index - - return True - - def holes(self): - '''Return holes in collection. - - Return :py:class:`~collection.Collection` of missing indexes. - - ''' - missing = set([]) - previous = None - for index in self.indexes: - if previous is None: - previous = index - continue - - if index != (previous + 1): - missing.update(range(previous + 1, index)) - - previous = index - - return Collection(self.head, self.tail, self.padding, indexes=missing) - - def is_compatible(self, collection): - '''Return whether *collection* is compatible with this collection. - - To be compatible *collection* must have the same head, tail and padding - properties as this collection. - - ''' - return all([ - isinstance(collection, Collection), - collection.head == self.head, - collection.tail == self.tail, - collection.padding == self.padding - ]) - - def merge(self, collection): - '''Merge *collection* into this collection. - - If the *collection* is compatible with this collection then update - indexes with all indexes in *collection*. - - raise :py:class:`~error.CollectionError` if *collection* is not - compatible with this collection. - - ''' - if not self.is_compatible(collection): - raise error.CollectionError('Collection is not compatible ' - 'with this collection.') - - self.indexes.update(collection.indexes) - - def separate(self): - '''Return contiguous parts of collection as separate collections. - - Return as list of :py:class:`~collection.Collection` instances. - - ''' - collections = [] - start = None - end = None - - for index in self.indexes: - if start is None: - start = index - end = start - continue - - if index != (end + 1): - collections.append( - Collection(self.head, self.tail, self.padding, - indexes=set(range(start, end + 1))) - ) - start = index - - end = index - - if start is None: - collections.append( - Collection(self.head, self.tail, self.padding) - ) - else: - collections.append( - Collection(self.head, self.tail, self.padding, - indexes=range(start, end + 1)) - ) - - return collections diff --git a/pype/vendor/clique/descriptor.py b/pype/vendor/clique/descriptor.py deleted file mode 100644 index 9cd4adbbba..0000000000 --- a/pype/vendor/clique/descriptor.py +++ /dev/null @@ -1,43 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - - -class Unsettable(object): - '''Prevent standard setting of property. - - Example:: - - >>> class Foo(object): - ... - ... x = Unsettable('x') - ... - ... def __init__(self): - ... self.__dict__['x'] = True - ... - >>> foo = Foo() - >>> print foo.x - True - >>> foo.x = False - AttributeError: Cannot set attribute defined as unsettable. - - ''' - - def __init__(self, label): - '''Initialise descriptor with property *label*. - - *label* should match the name of the property being described:: - - x = Unsettable('x') - - ''' - self.label = label - super(Unsettable, self).__init__() - - def __get__(self, instance, owner): - '''Return value of property for *instance*.''' - return instance.__dict__.get(self.label) - - def __set__(self, instance, value): - '''Set *value* for *instance* property.''' - raise AttributeError('Cannot set attribute defined as unsettable.') diff --git a/pype/vendor/clique/error.py b/pype/vendor/clique/error.py deleted file mode 100644 index 964e55348e..0000000000 --- a/pype/vendor/clique/error.py +++ /dev/null @@ -1,10 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -'''Custom error classes.''' - - -class CollectionError(Exception): - '''Raise when a collection error occurs.''' - diff --git a/pype/vendor/clique/sorted_set.py b/pype/vendor/clique/sorted_set.py deleted file mode 100644 index aa7e32157f..0000000000 --- a/pype/vendor/clique/sorted_set.py +++ /dev/null @@ -1,62 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -import collections -import bisect - - -class SortedSet(collections.MutableSet): - '''Maintain sorted collection of unique items.''' - - def __init__(self, iterable=None): - '''Initialise with items from *iterable*.''' - super(SortedSet, self).__init__() - self._members = [] - if iterable: - self.update(iterable) - - def __str__(self): - '''Return string representation.''' - return str(self._members) - - def __repr__(self): - '''Return representation.''' - return '<{0} "{1}">'.format(self.__class__.__name__, self) - - def __contains__(self, item): - '''Return whether *item* is present.''' - return self._index(item) >= 0 - - def __len__(self): - '''Return number of items.''' - return len(self._members) - - def __iter__(self): - '''Return iterator over items.''' - return iter(self._members) - - def add(self, item): - '''Add *item*.''' - if not item in self: - index = bisect.bisect_right(self._members, item) - self._members.insert(index, item) - - def discard(self, item): - '''Remove *item*.''' - index = self._index(item) - if index >= 0: - del self._members[index] - - def update(self, iterable): - '''Update items with those from *iterable*.''' - for item in iterable: - self.add(item) - - def _index(self, item): - '''Return index of *item* in member list or -1 if not present.''' - index = bisect.bisect_left(self._members, item) - if index != len(self) and self._members[index] == item: - return index - - return -1