Skip to content

Instantly share code, notes, and snippets.

@mauritsvanrees
Last active October 16, 2019 08:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mauritsvanrees/c47e974e418c707a626200cb6561405b to your computer and use it in GitHub Desktop.
Save mauritsvanrees/c47e974e418c707a626200cb6561405b to your computer and use it in GitHub Desktop.
Parse buildout config into constraints and requirements. See https://community.plone.org/t/creating-constraints-and-requirements-from-buildout-config/10296
# -*- coding: utf-8 -*-
# https://gist.github.com/mauritsvanrees/c47e974e418c707a626200cb6561405b
# https://community.plone.org/t/creating-constraints-and-requirements-from-buildout-config/10296
# from configparser import ConfigParser
from zc.buildout import buildout
import re
# We read this buildout config file:
configfile = "buildout.cfg"
# We cannot use configparser, because buildout configs contain 'duplicate' options:
# $ grep -i chameleon versions.cfg
# Chameleon = 3.6.2
# chameleon = 3.6.2
# So we use the buildout configparser:
config = buildout.Buildout(configfile, [])
# Get the constraints from the version pins.
# Ah, the versions get set directly on the config, nice!
# Note: this works like a dictionary, but is a class 'zc.buildout.buildout.Options'.
versions = config.versions
constraints_file = "parsed-constraints.txt"
with open(constraints_file, "w") as cfile:
cfile.write("# Constraints parsed from {}\n".format(configfile))
for package, version in sorted(versions.items()):
cfile.write("{}=={}\n".format(package, version))
print("Wrote versions as constraints to {}.".format(constraints_file))
# Now for the requirements.
# We must avoid automatically downloading eggs, so we use the raw config.
config = config._raw
reqs = set(["zc.buildout"])
extensions = config["buildout"].get("extensions", "").split()
reqs.update(set(extensions))
# Regular expression for ${test:eggs}.
# Probably needs some tweaks, but works for now.
re_option_reference = re.compile(r"\${(.*):(.*)}")
def get_option_reference(text):
# When text is ${test:eggs}, return ("test", "eggs").
match = re_option_reference.match(text)
if match is None:
return
return match.groups()
def read_eggs(section, option="eggs"):
# Get the eggs. Tricky is that these can be split on one line or on multiple lines.
# And there can be extras, either "package[extra]" or "package [extra]",
# making splitting harder.
result = set()
for line in config[section].get(option, "").splitlines():
line = line.strip()
if not line:
continue
# Get rid of spaces in front of an [extra].
line = "[".join([part.strip() for part in line.split("[")])
for egg in line.split():
# Because we use the raw buildout config, we may have eggs like this:
# ${test:eggs}
ref = get_option_reference(egg)
if not ref:
result.add(egg)
continue
other_section, other_option = ref
if other_section == ".":
other_section = section
eggs = read_eggs(other_section, other_option)
result.update(eggs)
return result
for section in config.keys():
data = config[section]
recipe = data.get("recipe")
if recipe:
reqs.add(recipe)
# Get the eggs options.
reqs.update(read_eggs(section))
requirements_file = "parsed-requirements.txt"
with open(requirements_file, "w") as rfile:
rfile.write("# Requirements parsed from {}\n".format(configfile))
for package in sorted(reqs):
rfile.write("{}\n".format(package))
print("Wrote requirements to {}.".format(requirements_file))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment