Package: javascript-utils
JS UtilsThis package contains python implementations of several npm packages I missed when moving from Node.js to Python. Currently the package contains implementions of theconfignpm package as well asstubsfrom thesinonpackage.ConfigConverts json files in a config directory into a Config NamedTuple. Based off the npm packageconfig.UsageWithin a configuration directory (./configby default), provide a json configuration file
for each environment your application will run in. For example, your project structure might look like:.
└── project/
    ├── config/
    │   ├── develop.json
    │   ├── production.json
    │   ├── qa.json
    │   └── test.json
    ├── jsutils/
    │   └── project files
    └── test/
        └── test filesWhere each file in the config directory has contents like:{"int_key":13,"array_key":['a','b','c'],"object_key":{"application_name":"jsutils","application_creator":"me"}}Then from your application:fromconfigimportget_configconfig=get_config()print(config)# Config(int_key=13, array_key=('a', 'b', 'c'), object_key=ObjectKey(application_name='jsutils', application_creator='me')By default, the module will load the configuration file corresponding to the value inos.environ['ENV']. If no value is present, it will default to'test'. If there is no file for the specified (or default) environment, anImportExceptionwill be raised. The environment can be passed directly to the factory function as can the directory.fromconfigimportget_configconfig=get_config(directory='./deep/config/directory/',environment='production')DefaultsA default file can be used to provide values that are static accross environments. If keys in the environment files duplicate keys in the default file, the environment file values will be used. For example, if your config directory had the structure:.
└── config/
    ├── default.json
    └── test.jsonAnd default.json had contents{"default_key":"always the same","override_key":"default value"}While test.json had contents{"override_key":"overridden value","environment_specific_key":"something test environment specific"}Then withenvironment='test', the config object would have the following attributes and values:config.default_key# "always the same"config.overriden_key# "overridden value"config.environment_specific_key# "something test environment specifc"Custom Environment VariablesAn optional custom environment variables file can be added to add environment variables to your configuration object at the time of construction. Your config directory would look like:.
└── config/
    ├── custom_environment_variables.json
    ├── default.json
    └── test.jsonAnd default.json had contents{"default_key":"always the same","override_key":"default value"}While test.json had contents{"override_key":"overridden value","environment_specific_key":"something test environment specific"}And custom_environment_variables.json had contents{"environment_key_1":"ENV_KEY_1","nested_environment_keys":{"nested_key_1":"NESTED_ENV_KEY_1"}}Then withenvironment='test', the config object would have the following attributes and values:config.default_key# "always the same"config.overriden_key# "overridden value"config.environment_specific_key# "something test environment specifc"config.environment_key_1# value at os.environ['ENV_KEY_1']config.nested_environment_keys.nested_key_1# value at os.environ['NESTED_ENV_KEY_1].hasand.getThe Config class comes prebuilt withhasandgetmethods that can be used to test if a property is present and to fetch a property. These methods also accept "deep" paths to objects.For example, if youdefault.jsonlooked like:{"top_level_key":"I'm at the top","nested_top":{"nested_key":"It's safe inside"}}Then you can test for and fetch the nested propertynested_top.nested_keyas follows:config.has('nested_top.nested_key')# Trueconfig.get('nested_top.nested_key')# "It's safe inside"Future DevelopmentSupport for Additional File Types: Sincejsonis not commonly used in python applications for configuration, to make this library more broadly appealing, I would like to support additional file types such as.yaml,.ini,.cfg, and others.Secrets Addons: I would like to make the library extensible so that if you are managing secrets with a third party system like Vault or AWS Secrets Manager, addons could provide ways to fetch those values into your configuration object.StubsThis class and its factory function provides an alternative to the Mock objects fromunittest.mock. In particular the behavior of these stubs is more flexible and versatile. Based off the stubs in the npm packagesinon.The Stub classThe stub can be instantiated with a set of attributes.fromstubsimporget_stub,Stubstub1=Stub(attrs={'prop1':'value1','prop2':1.61803})stub1.prop1# 'value1'stub1.prop2# 1.61803stub2=get_stub(attrs={'prop3':3.14159,'prop4':False})stub2.prop3# 3.14159stub2.prop4# FalseDefault BehaviorStubs can be configured to return a specific value or to raise an exception by default. If both are configured, the exception will be raised.fromstubsimportget_stubstub1=get_stub()stub1.returns('Hello there')result1=stub()result1# 'Hello there'stub2=get_stub()stub2.raises(RuntimeError('Oh no'))stub2()# RuntimeError: Oh nostub3=get_stub()stub3.raises(RuntimeError('Its broken'))stub3.returns('Oh happy day')stub3()# RuntimeError: Its brokenIf behavior for a stub has not been configured, it will returnNonewhen called.Asserting on CallsThe stub has a helper method to assert that a call with a specific signature was made to the stub. If you need to assert that a specific call (i.e. the seventh call) was made with a specific signature, you'll have to fall back to using the stub's call history attribute. Stubs are compatible with the ANY value from unittest.mock.fromunittest.mockimportANYfromstubsimportget_stubdeftest_should_know_all_calls():stub=get_stub()stub.returns('some_value')stub('first call')assertstub.called_with('first call')stub('second call',second_call_kwarg=1.414)assertstub.called_with('second call',second_call_kwarg=1.414)assertstub.call_history[1]==(('second_call',),{'second_call_kwarg':1.414})assertstub.called_with(ANY,second_call_kwarg=1.414)Fake BehaviorIf your stub needs more complicated behavior, you can provide a function that the stub will pass the arguments to. The fake behavior takes priority over all other behaviors.fromstubsimportget_stubdeffake_func(arg1,args2):returnargs1+arg2stub=get_stub()stub.calls_fake(fake_func)stub(1,2)# 3stub('first','second')# 'firstsecond'Behavior on Specific CallsThe stub can be configured to return a value or raise an exception on a specified 0 index based call. The stub will fall back to its default behavior if the specified call has not been configured. There are convenience functions for setting first, second, and third call behavior. Method chaining is required for proper use.fromstubsimportget_stubstub=get_stub()stub.return('default_value')stub.on_call(1).returns('second_call_value')stub.on_third_call().returns('third_call_value')stub.on_call(3).raises(RuntimeError('Boom'))stub()# 'default_value'stub()# 'second_call_value'stub()# 'third_call_value'stub()# RuntimeError: Boom raisedstub()# 'default_value'Behavior for Specific Call SignaturesThe stub can be configured to have specific behavior for specific call arguments. This behavior can be based on a call count or fall back to a default behavior. Call signature behavior supercedes call count and default behavior when applicable, but is superceded in priority by fake behaviors. Method chaining is required for proper use.fromstubsimportget_stubstub=get_stub()stub.with_args('first',second_arg='second').returns(42)stub.with_args('first',second_arg='second').on_call(1).returns(0)stub.returns('i')stub()# 'i'stub('first',second_arg='second')# 42stub('first',second_arg='second')# 0stub('first',second_arg='second')# 42Overriding Methods on ObjectsThe Stub class and get_stub function can also be used to replace a method on an existing object with a stub. The created original behavior of the methods can be restored with the restore method.fromstubsimportget_stub,StubclassSomeClass:defmethod_1(self):return'hello'defmethod_2(self):return'goodbye'a_class=someClass()stub_1=get_stub(a_class,'method_1')stub_1.returns('hola')a_class.method_1()# 'hola'stub_2=Stub(a_class,'method_2')stub_2.returns('adios')a_class.method_2()# 'adiosstub_1.restore()stub_2.restore()a_class.method_1()# 'hello'a_class.method_2()# 'goodbye'