Skip to content

Commit

Permalink
Add profile handling to configparser
Browse files Browse the repository at this point in the history
Move it from Buildozer.
Rename it to be more meaningful.
Add support for whitespace trimming.
Update documentation in default.spec.
Update tests.
  • Loading branch information
Julian-O committed Jul 24, 2023
1 parent 8c8ea02 commit 1f703f7
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 82 deletions.
39 changes: 4 additions & 35 deletions buildozer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def __init__(self, filename='buildozer.spec', target=None):
self.specfilename = filename
self.state = None
self.build_id = None
self.config_profile = ''
self.config = SpecParser()

self.logger = Logger()
Expand Down Expand Up @@ -900,6 +899,8 @@ def run_default(self):
self.run_command(cmd)

def run_command(self, args):
profile = None

while args:
if not args[0].startswith('-'):
break
Expand All @@ -913,13 +914,13 @@ def run_command(self, args):
exit(0)

elif arg in ('-p', '--profile'):
self.config_profile = args.pop(0)
profile = args.pop(0)

elif arg == '--version':
print('Buildozer {0}'.format(__version__))
exit(0)

self._merge_config_profile()
self.config.apply_profile(profile)

self.check_root()

Expand Down Expand Up @@ -1034,35 +1035,3 @@ def cmd_serve(self, *args):
print("Serving via HTTP at port {}".format(SIMPLE_HTTP_SERVER_PORT))
print("Press Ctrl+c to quit serving.")
httpd.serve_forever()

#
# Private
#

def _merge_config_profile(self):
profile = self.config_profile
if not profile:
return
for section in self.config.sections():

# extract the profile part from the section name
# example: [app@default,hd]
parts = section.split('@', 1)
if len(parts) < 2:
continue

# create a list that contain all the profiles of the current section
# ['default', 'hd']
section_base, section_profiles = parts
section_profiles = section_profiles.split(',')
if profile not in section_profiles:
continue

# the current profile is one available in the section
# merge with the general section, or make it one.
if not self.config.has_section(section_base):
self.config.add_section(section_base)
for name, value in self.config.items(section):
print('merged ({}, {}) into {} (profile is {})'.format(name,
value, section_base, profile))
self.config.set(section_base, name, value)
60 changes: 20 additions & 40 deletions buildozer/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -415,48 +415,11 @@ warn_on_root = 1
# (str) Path to build output (i.e. .apk, .aab, .ipa) storage
# bin_dir = ./bin

# -----------------------------------------------------------------------------
# List as sections
#
# You can define all the "list" as [section:key].
# Each line will be considered as a option to the list.
# Let's take [app] / source.exclude_patterns.
# Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
# This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


# -----------------------------------------------------------------------------
# Profiles
#
# You can extend section / key with a profile
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

### Notes about using this file: ###
#-----------------------------------------------------------------------------
# Notes about using this file:
#
# Buildozer uses a variant of Python's ConfigSpec to read this file.
# For the syntax, including interpolations, see
# For the basic syntax, including interpolations, see
# https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
#
# Warning: Comments cannot be used "inline" - i.e.
Expand Down Expand Up @@ -486,3 +449,20 @@ warn_on_root = 1
# Buildozer supports overriding options through environment variables.
# Name an environment variable as SECTION_OPTION to override a value in a .spec
# file.
#
# Buildozer support overriding options through profiles.
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
# [app@demo]
# title = My Application (demo)
#
# [app:source.exclude_patterns@demo]
# images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
# buildozer --profile demo android debug
#
# Environment variable overrides have priority over profile overrides.
6 changes: 4 additions & 2 deletions buildozer/scripts/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@

class BuildozerRemote(Buildozer):
def run_command(self, args):
profile = None

while args:
if not args[0].startswith('-'):
break
Expand All @@ -45,7 +47,7 @@ def run_command(self, args):
self.logger.log_level = 2

elif arg in ('-p', '--profile'):
self.config_profile = args.pop(0)
profile = args.pop(0)

elif arg in ('-h', '--help'):
self.usage()
Expand All @@ -55,7 +57,7 @@ def run_command(self, args):
print('Buildozer (remote) {0}'.format(__version__))
exit(0)

self._merge_config_profile()
self.config.apply_profile(profile)

if len(args) < 2:
self.usage()
Expand Down
67 changes: 62 additions & 5 deletions buildozer/specparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
- either comma separated, or in their own [section:option] section.
- environment variable overrides of values
- overrides applied at construction.
- profiles
- case-sensitive keys
- "No values" are permitted.
"""

from configparser import ConfigParser
from os import environ

from buildozer.logger import Logger


class SpecParser(ConfigParser):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -49,8 +52,14 @@ def read_dict(self, dictionary, source="<dict>"):
# Add new getters

def getlist(
self, section, token, default=None, with_values=False, strip=True,
section_sep="=", split_char=","
self,
section,
token,
default=None,
with_values=False,
strip=True,
section_sep="=",
split_char=",",
):
"""Return a list of strings.
Expand Down Expand Up @@ -83,8 +92,8 @@ def getlist(
return values if not strip else [x.strip() for x in values]

def getlistvalues(self, section, token, default=None):
""" Convenience function.
Deprecated - call getlist directly."""
"""Convenience function.
Deprecated - call getlist directly."""
return self.getlist(section, token, default, with_values=True)

def getdefault(self, section, token, default=None):
Expand All @@ -99,7 +108,55 @@ def getbooldefault(self, section, token, default=False):
Deprecated - call getboolean directly."""
return self.getboolean(section, token, fallback=default)

# Handle env vars.
def apply_profile(self, profile):
"""
Sections marked with an @ followed by a list of profiles are only
applied if the profile is provided here.
Implementation Note: A better structure would be for the Profile to be
provided in the constructor, so this could be a private method
automatically applied on read *before* _override_config_from_envs(),
but that will require a significant restructure of Buildozer.
Instead, this must be called by the client after the read, and the env
var overrides need to be reapplied to the relevant options.
"""
if not profile:
return
for section in self.sections():

# extract the profile part from the section name
# example: [app@default,hd]
parts = section.split("@", 1)
if len(parts) < 2:
continue

# create a list that contain all the profiles of the current section
# ['default', 'hd']
section_base, section_profiles = parts
section_profiles = section_profiles.split(",")

# Trim
section_base = section_base.strip()
section_profiles = [profile.strip() for profile in section_profiles]

if profile not in section_profiles:
continue

# the current profile is one available in the section
# merge with the general section, or make it one.
if not self.has_section(section_base):
self.add_section(section_base)
for name, value in self.items(section):
Logger().debug(
"merged ({}, {}) into {} (profile is {})".format(
name, value, section_base, profile
)
)
self.set(section_base, name, value)

# Reapply env var, if any.
self._override_config_token_from_env(section_base, name)

def _override_config_from_envs(self):
"""Takes a ConfigParser, and checks every section/token for an
Expand Down
51 changes: 51 additions & 0 deletions tests/test_specparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def test_overrides(self):
sp.read([spec_path])
assert sp.get("section.1", "attribute.1") == "Env Value"

del environ["SECTION_1_ATTRIBUTE_1"]

def test_new_getters(self):
sp = SpecParser()
sp.read_string(
Expand Down Expand Up @@ -118,6 +120,55 @@ def test_case_sensitivity(self):
assert sp.get("section1", "attribute1") == "a"
assert sp.get("section1", "Attribute1") == "A"

def test_profiles(self):
sp = SpecParser()
sp.read_string(
"""
[section1]
attribute1=full system
[section1 @demo1, demo2]
attribute1=demo mode
"""
)

# Before a profile is set, return the basic version.
assert sp.get("section1", "attribute1") == "full system"

# Empty profile makes no difference.
sp.apply_profile(None)
assert sp.get("section1", "attribute1") == "full system"

# Inapplicable profile makes no difference
sp.apply_profile("doesn't exist")
assert sp.get("section1", "attribute1") == "full system"

# Applicable profile changes value
sp.apply_profile("demo2")
assert sp.get("section1", "attribute1") == "demo mode"

def test_profiles_vs_env_var(self):
sp = SpecParser()

environ["SECTION1_ATTRIBUTE1"] = "simulation mode"

sp.read_string(
"""
[section1]
attribute1=full system
[section1@demo1,demo2]
attribute1=demo mode
"""
)

# Before a profile is set, env var should win.
assert sp.get("section1", "attribute1") == "simulation mode"

# Applicable profile: env var should still win
sp.apply_profile("demo1")
assert sp.get("section1", "attribute1") == "simulation mode"

del environ["SECTION1_ATTRIBUTE1"]

def test_controversial_cases(self):
"""Some aspects of the config syntax seem to cause confusion.
This shows what the code is *specified* to do, which might not be
Expand Down

0 comments on commit 1f703f7

Please sign in to comment.