Package: ezenv
“For easy access, baby!  …That’s right.’”—Eazy-EHave you ever thought handling environment variables in Python should be easier?
It is now.TL;DR:>>>importenv>>>env.SERVER_PORT.int8080“BAM!” —Emeril LagasseThe E.Z.E.nvironment module also has its owntheme song:“We want Eazy!”EAZY!Everybody come on!EAZY!Who yall came to see?EAZY!A little louder come on!EAZY!Get those hands in the air!EAZY!Come on, come on say it!EAZY!A little louder come on!EAZY!Come on make some noise!A miracle of modern creation…EZ E’s on the set, hyped up with the bassAnd a little bit of what ya loveFrom a brother who’s smooth like a criminalI mean subliminal…BackgroundIt’s always been a tad clumsy to access environment variables and combine them
with other strings in Python—compared to shell languages at least.
For example, look how easy it is in (ba)sh:⏵echo"Libraries:$PWD/lib"Libraries:/usr/local/libUnfortunately over in Python-land,
required, escaped quotes and brackets
serve mostly to complicate and add to visual clutter,
reducing speed of comprehension.>>>fromosimportenviron>>>fromos.pathimportjoin>>>join(environ['PWD'],'lib')'/usr/local/lib'Even the new-fangled string interpolation doesn’t help as much as might be
expected:>>>print(f'Libraries:{environ["PWD"]}/lib')Libraries:/usr/local/libWith that in mind, allow me to introduce theenvmodule.
With it I’ve tried to whittle complexity down,
primarily through direct attribute access:>>>importenv>>>print('Term:',env.TERM)Term:xterm-256color>>>print(f'Libraries:{env.PWD}/lib')Libraries:/usr/local/libBut wait, there’s more!Install⏵pip3install--userezenv# env was taken :-/☛ LGPL licensed. ☚Environment and optionsOn import the module loads the environment into its namespace,
thereby working like a dictionary with convenient attribute access.So, no additional mapping instance has to be created or imported,
unless you’d like to configure the interface further.
The following options are available to customize:>>>fromenvimportEnvironment>>>env=Environment(environ=os.environ,sensitive=True|False,# case: platform defaultwritable=False,pathsep=os.pathsep,)Param: environA mapping of your own choosing may optionally be passed in as the first argument,
for testing and/or other purposes.
I’ve recently learned thatos.environb(bytes interface) is a thing that could be passed,
for example.Param: writableBy default the Environment object/module does not allow modification since
writing is rarely needed.
This default helps to remind us of that fact,
though the object can be easily be changed to writable if need be by enabling
this option.Param: sensitivity 😢Variables are case-sensitive by default on Unix,insensitiveunder Windows.While case sensitivity can be disabled to use variable names in mixed or
lower-case,
be aware that variables and dictionary methods are in the same namespace,
which could potentially be problematic if they are not divided by case.
For this reason, using variable names such as “keys” and “items”
are not a good idea while in insensitive mode.shrugWorkaround: use “get item” / dictionary-style syntax if needed:env['keys']# :-/Entry ObjectsWhile usingenvat the interactive prompt,
you may be surprised that a variable value is not a simple string but rather
an extended string-like object called an “Entry.”
This is most evident at the prompt since it prints a “representation”
form by default:>>>env.PWD# a.k.a. repr()Entry('PWD','/usr/local')The reason behind this custom object is so that the variables can offer
additional functionality,
such as parsing or conversion of the value to another type,
while not crashing on a non-existent attribute access.No matter however,
as we’ve seen in the previous sections,
just about any operation renders the string value as normal.
Attributes.nameand.valueare also available for belt &
suspenders types:>>>print(env.PWD)/usr/local>>>env.PWD.name,env.PWD.value,str(env.PWD)('PWD','/tmp','/tmp')Remember theenvobject/module is also a standard dictionary,
while entry values are also strings,
so full Python functionality is available:>>>forkey,valueinenv.items():# it's a dict*print(key,value)# USER fred…>>>env.USER.title()# it's a str*'Fred'>>>env.TERM.partition('-')# tip: a safer split('xterm','-','256color')*  Sung to the tune,“It’s a Sin,”by the Pet Shop Boys.Conversions & ParsingAnother handy feature of Entry objects is convenient type conversion and
parsing of values from strings.
Additional properties for this functionality are available.
For example:>>>env.PI.float3.1416>>>env.STATUS.int5150>>>env.DATA.from_json{'one':1,'two':2,'three':3}Truthy ValuesVariable entries may contain boolean-likestring values,
such as0, 1, yes, no, true, false, etc.
To interpret them in a case-insensitive manner use the.truthyproperty:>>>env.QT_ACCESSIBILITYEntry('QT_ACCESSIBILITY','1')>>>env.QT_ACCESSIBILITY.truthyTrue>>>env=Environment(writable=True)>>>env.QT_ACCESSIBILITY='0'# set to '0'>>>env.QT_ACCESSIBILITY.truthyFalseStandard Boolean TestsAs always, standard tests orbool()on the entry can be done to check a
string.
Remember, such a test checks merely if the string is empty or not,
and would also returnTrueon'0'or'false'.PathsEnvironment vars often contain a list of filesystem paths.
To split such path strings onos.pathsep🔗,
with optional conversion topathlib.Path🔗²objects,
use one or more of the following:>>>env.XDG_DATA_DIRS.list['/usr/local/share','/usr/share',...]# strings>>>env.SSH_AUTH_SOCK.pathPath('/run/user/1000/keyring/ssh')>>>env.XDG_DATA_DIRS.path_list[Path('/usr/local/share'),Path('/usr/share'),...]To split on a different character,
simply do the split/partition on the string manually.ExamplesThere are generally three cases for environment variables:Variable exists, has value:>>>env.USER# exists, reprEntry('USER','fred')>>>env.USER+'_suffix'# str ops'fred_suffix'>>>env.USER.title()# str ops II'Fred'>>>print(f'term:{env.TERM}')# via interpolationterm:xterm-256color>>>bool(env.USER)# check exists & not emptyTrue>>>key_name='PI'>>>env[key_name]# getitem syntax'3.1416'>>>env.PI.float# type conversion3.1416>>>env.PORT.intor9000# type conv. w/ default5150>>>env.QT_ACCESSIBILITY.truthy# 0/1/yes/no/true/falseTrue>>>env.JSON_DATA.from_json.keys()['one','three','two']>>>env.XDG_DATA_DIRS.list['/usr/local/share','/usr/share']Variable exists, but is blank:>>>'EMPTY'inenv# check existenceTrue>>>env.EMPTY# exists but emptyEntry('EMPTY','')>>>bool(env.EMPTY)# check exists & not emptyFalse>>>env.EMPTYor'default'# exists, blank w/ default'default'Variable doesn’t exist:>>>'NO_EXISTO'inenv# check existenceFalse>>>env.NO_EXISTOor'default'# DNE with default'default'>>>env.NO_EXISTO# Doesn't exist reprNullEntry('NO_EXISTO')>>>bool(env.NO_EXISTO)# check exists & not emptyFalse>>>env.XDG_DATA_DIRz.list# DNE fallback[]fordata_dirinenv.XDG_DATA_DIR.list:# Don't need to worry if this exists or not,# if not, it will be skipped.passCompatibility“What’s the frequency Kenneth?”This module attempts compatibility with KR’s existingenvpackage by implementing itsprefixandmapfunctions:>>>env.prefix('XDG_')# from_prefix preferred{'config_dirs':'/etc/xdg/xdg-mate:/etc/xdg',...}>>>env.map(username='USER'){'username':'fred'}The lowercase transform can be disabled by passing another false-like value
as the second argument toprefix().While the package above has the covetedenvnamespace on PyPI,
ezenv uses the same simple module name and provides an implementation of the
interface.TestsCan be run here:⏵python3-menv-vThough this module works under Python2,
several of the testsdon’t,
because Py2 does Unicode differently or
doesn’t have the facilities available to handle them by default
(pathlib/f-string).
Haven’t had the urge to work around that due to declining interest.FYI, a reference to the original module object is kept atenv._modulejust in case it is needed for some reason.TestingwithezenvWhen you’ve usedezenvin your project,
it is easy to create a custom environment to operate under:fromenvimportEnvironmentdeftest_foo():importmymodulemymodule.env=Environment(environ=dict(NO_COLOR='1'))assertmymodule.color_is_disabled()==TruePricing“I’d buy THAT for a dollar!” :-D