Skip to content

Instantly share code, notes, and snippets.

@elliottwilliams
Created September 24, 2019 21:44
Show Gist options
  • Save elliottwilliams/fdf7730ef06809abeb88299a97d57ffa to your computer and use it in GitHub Desktop.
Save elliottwilliams/fdf7730ef06809abeb88299a97d57ffa to your computer and use it in GitHub Desktop.
Hashing a Cartfile dependency's version with the versions of its subdependencies
from hashlib import sha1
from collections import namedtuple
def version_hash_for_dependency(dependency, dependency_graph):
"""
Returns a hex string created by hashing together the versions of
`dependency` with the versions of its dependencies and subdependencies as
specified in `dependency_graph`. For example, given the dependency graph:
A -> B D
B -> C
C ->
D ->
E -> A
hash(A) = hash(A B C D)
hash(B) = hash(B C)
hash(C) = hash(C)
hash(D) = hash(D)
hash(E) = hash(E A B C D)
"""
def update_hasher_for_dependency(dependency, dependency_graph, hasher):
hasher.update(dependency.version_requirement.encode('utf8'))
for subdependency in dependency_graph[dependency]:
update_hasher_for_dependency(subdependency, dependency_graph, hasher)
hasher = sha1()
update_hasher_for_dependency(dependency, dependency_graph, hasher)
return hasher.hexdigest()
import re
from collections import defaultdict
from collections import deque
from collections import namedtuple
class Dependency(namedtuple('Dependency', 'source origin version_requirement')):
"""
A data type that represents a line from a Cartfile. For example:
github "antitypical/Result" ~> 4.0.0
is
Dependency(
source='github',
origin='"antitypical/Result"',
version_requirement='~> 4.0.0'
)
"""
@property
def name(self):
"""
Returns the checkout name of the dependency by taking the last
component of the origin and removing any path extension.
"""
basename = self.origin.strip('"').split('/')[-1]
name = basename.partition('.')[0]
return name
def pinned_version(self):
"""
If the version requirement is a pinned version, return the version
string.
"""
match = re.match(r'"(.+)"', self.version_requirement)
if match:
return match.group(1)
def __str__(self):
return self.name
def read_cartfile(path):
"""
Given a pathlib.Path to a Cartfile, yield its dependencies.
"""
lines = path.read_text('utf8').splitlines()
yield from (
dependency for dependency in
(parse_dependency(line) for line in lines)
if dependency is not None
)
def parse_dependency(line):
"""
Sanitize a line of text from a Cartfile and return a Dependency
representing that line, if the line contains a dependency.
"""
comment_marker = line.find('#')
if comment_marker > -1:
line = line[:comment_marker]
line = line.strip()
if line:
source, origin, *version_components = line.split()
return Dependency(source, origin, ' '.join(version_components))
def dependencies_for_dependency(dependency, checkouts_dir):
"""
Yields dependencies from the Cartfile of a given dependency. Assumes the
dependency has been checked out if it's not binary.
"""
checkout = checkouts_dir / dependency.name
if dependency.source == 'binary':
return # yield nothing
assert checkout.exists(), f'{dependency.name} not checked out in {checkouts_dir}'
dependency_cartfile = checkout / 'Cartfile'
if dependency_cartfile.exists():
yield from read_cartfile(dependency_cartfile)
def _dependency_graph_edges(cartfile, checkouts_dir):
"""
Yields 2-tuples for all of the dependency relationships given by a
`cartfile` and any additional Cartfiles contained in `checkouts_dir`. The
tuple
(Node, Leaf)
means that Node dependes on Leaf.
"""
visited_names = set()
q = deque(read_cartfile(cartfile))
while q:
dependency = q.popleft()
if dependency.name not in visited_names:
visited_names.add(dependency.name)
for subdependency in dependencies_for_dependency(dependency, checkouts_dir):
yield (dependency, subdependency)
q.append(subdependency)
def dependency_graph_for_cartfile(cartfile, resolved_cartfile, checkouts_dir):
"""
Returns a dictionary representing the relationships of a given `cartfile`
and the Cartfiles contained in `checkouts_dir`. The dictionary maps
Dependencies to lists of Dependencies at the pinned versions given in
`resolved_cartfile`.
"""
graph = defaultdict(list)
resolved_versions = {
dependency.name: dependency
for dependency in read_cartfile(resolved_cartfile)
}
for dependency, subdependency in _dependency_graph_edges(cartfile, checkouts_dir):
resolved_dependency = resolved_versions[dependency.name]
resolved_subdependency = resolved_versions[subdependency.name]
graph[resolved_dependency].append(resolved_subdependency)
return graph
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment