Repository: PH-Tools/honeybee_ph (language: python)

Issue:
Brainstorming extending extension
Hiya... prolly not best use of a PR but figured:
    not exactly not a good use of a PR?  less 'discussing' more 'just write some shit that works than make it better'  #working_on_disfunction :D

Can you think of a way to test this explicitly?
I'm thinking a straight up:

Apply it to a test case:
   try and set a _energy._ph property:
      if aforementioned property exists:
          print('itfuckinworked')
      else:
          print('SOL')
       


Discussion / hints:
OHHHHHHH:::: HBE doesn't have: [extensionutil.py](https://github.com/ladybug-tools/honeybee-core/blob/master/honeybee/extensionutil.py) Hmmmm.... I feel like Chris may be keen to add that to HBE? or could PR that? whachya think?

Exactly. Honeybee objects all have a .properties, and that whole method they use to extend the .properties when its first initialized. But Extensions like Honeybee-Energy and Honeybee-Radiance, and also base Ladybug Objects like Vertex of Face3D, do not have a .properties that can be extended by default. 

In order to test my wufi writer, I implemented a subclassing phase where I allowed a .properties to be added. But that only works if you explicitly convert the base objects over the new subclassed honeybee-ph objects. See [here](https://github.com/PH-Tools/honeybee_ph/blob/eb43dec1f96f382c710f7196b92fd78a13114ca9/main.py#L25), [here](https://github.com/PH-Tools/honeybee_ph/blob/eb43dec1f96f382c710f7196b92fd78a13114ca9/from_HBJSON/convert_HBJSON.py#L138) and [here](https://github.com/PH-Tools/honeybee_ph/blob/eb43dec1f96f382c710f7196b92fd78a13114ca9/honeybee_energy_ph/construction/opaque.py#L21) for an example.

So right now that only works outside GH. This method only works if you read in an HBJSON and it'll convert it over to my new subclassed object types. But of course the goal is to have the ability to add .PH attributes to things like honeybee-energy schedules inside GH, so will need to come up with some kind of method for this. 

But yeah, I think this was where I got a little stuck a few weeks back and put it down for a little bit. Gotta dive back in at some point here. It seems the issue is that the 'extensions' like hb-energy were not designed to be 'extended' themselves in the same way honeybee-core was though?
@PH-Tools I mean: Want me to add what's needed to HBE and do a PR so can use like HB core?  
I guess it would need to have a whole thing where the extension does a preload, like the hb-core does with it's [`__init__` where it looks for other packages with a 'honeybee' prefix and adds them in](https://github.com/ladybug-tools/honeybee-core/blob/1dc780de12a571a07f76fc9077cf057d9f9b34a7/honeybee/__init__.py#L9-L20). Right? 

And then I suppose it would need to have an `_extends_honeybee` file as well? Not sure if that would work. I'm not 100% sure who / where the `_extends_honeybee` gets called? If you added an `_extends_honeybee_energy` files, I'm not sure what would happen? It'd probably just ignore it?

But yeah, in principle I think that will be the road we'll need to go down? But that means that maybe we're proposing a whole new rule? That all 'extensions' also need to implement an 'extension' loading sequence of their own? Which seems fine, but is a change for sure. 
Wait! I think I'm totally wrong. Looks like it is already in there....

[https://github.com/ladybug-tools/honeybee-energy/blob/31e88614b747458403bf6de85a8f016157125056/honeybee_energy/__init__.py#L16](https://github.com/ladybug-tools/honeybee-energy/blob/31e88614b747458403bf6de85a8f016157125056/honeybee_energy/__init__.py#L16)

... interesting....
but for it to work, the `honyebee_energy_ph` package needs to be up at the top level, not down inside the `honeybee_ph` dir I think...? I guess also, that only updates the HB-Core objects `.properties` such that it includes a `.energy` slot. That doesn't actually add a `.properties` to objects like Schedules and Materials that are missing them?
I think I sort of see now. Writing it down so I can rememeber later. 

so it 
1) When Honeybee-Core is imported for the first time, [during `__init__` it will search for any package with a 'honeybee_' prefix and load that package as an extension.](https://github.com/ladybug-tools/honeybee-core/blob/3b01f32b9a67933c73a2a3569f4aaaa72e8f8a6f/honeybee/__init__.py#L12) 
2) When it finds one, like `honeybee_energy`, it loads the package, which calls the `honeybee_energy.__init__.py` file. 
3) Within `honeybee_energy.__init__.py`, it loads [`_honeybee_energy._extend_honeybee`](https://github.com/ladybug-tools/honeybee-energy/blob/31e88614b747458403bf6de85a8f016157125056/honeybee_energy/__init__.py#L8)
4) Inside [`honeybee_energy.__init__.py`](https://github.com/ladybug-tools/honeybee-energy/blob/31e88614b747458403bf6de85a8f016157125056/honeybee_energy/_extend_honeybee.py) it furkles with a bunch of the classes, adding new .properties items, in this case .energy

so if I add my PH new packages alongside the existing honeybee ones, it seems to work for the base HB objects at least, and I get a .ph slot now:

<img width="1211" alt="Screen Shot 2021-12-05 at 8 48 36 PM" src="https://user-images.githubusercontent.com/69652712/144774658-4db41b8f-863e-45bb-86d7-8e06ad42371a.png">

so I wonder if its possible to edit the honeybee-energy classes using this method, rather than subclass them, as I was doing before? hmm....


AHHHHHHH CHOICE!!
I think that ties together:
![image](https://user-images.githubusercontent.com/63176386/144853937-b9d06cf0-0771-4730-814f-289b6cbeb860.png)
in abstract.

I don't think Chris would be opposed to stuff like Sch/Mats etc being added some code to adress:
```That doesn't actually add a .properties to objects like Schedules and Materials that are missing them?``` if needed?
still wrapping my head abouts the finer grain bits 

Yeah that makes sense. I think that 'extending the extension' seems to work fine using the built-in `_extend_honeybee_*` method there. The sticky issue is how to deal with the classes like [`Lighting`](https://github.com/ladybug-tools/honeybee-energy/blob/86cf774be6e6df597900e1ba17c9d034c0c9fbc6/honeybee_energy/load/lighting.py#L16) and other HB-Energy objects which don't gave a `.properties` to extend...

Was just noodling around. It seems like it *could* work, if  you add a `.properties` to the HB-Energy objects


<img width="1168" alt="Screen Shot 2021-12-06 at 10 52 32 AM" src="https://user-images.githubusercontent.com/69652712/144878219-3779553f-b798-43d9-b527-fee7dab06ff1.png">

That actually seems to work ok, and then you can get access to the .properties within GH, and conceivable could extend it with a `.ph` or whatever extension, same as the core HB objects maybe?

<img width="1175" alt="Screen Shot 2021-12-06 at 10 55 28 AM" src="https://user-images.githubusercontent.com/69652712/144878514-00778c38-c78b-4e88-a6c1-7a053aecf7cf.png">


I guess the other method would be to subclass HB-Energy's Lighting as `PH_Lighting` or something like that, and then just add the .properties slot there (or ignore the properties thing entirely, and just do what we need for PH at the normal class level). So making a new GH component for 'PH Lighting' that makes a different Lighting object. I think the issue there is what / when the user makes a Lighting object somewhere else? I won't be 'PH_Lighting' object. Could cause all sorts of issues I'd think?


Example of the subclassing method:

<img width="1395" alt="Screen Shot 2021-12-06 at 11 49 55 AM" src="https://user-images.githubusercontent.com/69652712/144887429-dab2aa16-0374-4898-ade2-1c0d043e80bc.png">

But yeah, the problem is
1) Everytime you want to interact, you have to check if is_PH_object
2) If the user makes a Lighting object anywhere but with this special constructor, will run into problems
3) Not sure what happens to this type of obj during a room's `duplicate()` ? or sim?

hmmmm duplicate.... I kinda went around the rosies on that to add user_data to everything: so I'ma 'attempt to explain it as to share everything I know about it' incase that uncovers anything helpful:

in the root class of every HBE category: in this instance hbe/load/_base.py:
 
[def duplicate](https://github.com/ladybug-tools/honeybee-energy/blob/86cf774be6e6df597900e1ba17c9d034c0c9fbc6/honeybee_energy/load/_base.py#L79)

```python
def duplicate(self):
   return self.__copy__()
   
def __copy__(self):
        new_obj = _LoadBase(self.identifier)
        new_obj._display_name = self._display_name
        new_obj._user_data = None if self._user_data is None else self._user_data.copy()
        return new_obj
```

Then if we go over to lighting: there is no duplicate() function stated but it's inherited, so the lighting loads copy is used and that's:

```python
def __copy__(self):
        new_obj = Lighting(
            self.identifier, self.watts_per_area, self.schedule,
            self.return_air_fraction, self.radiant_fraction, self.visible_fraction)
        new_obj._display_name = self._display_name
        new_obj._baseline_watts_per_area = self._baseline_watts_per_area
        new_obj._user_data = None if self._user_data is None else self._user_data.copy()
        return new_obj
```
sooo in attempt to tie it all together: if [we go to honeybee-core _base](https://github.com/TfedUD/honeybee-core/blob/3b01f32b9a67933c73a2a3569f4aaaa72e8f8a6f/honeybee/_base.py#L94), as a traceback to see where 'rooms' duplicate() is rooted:
it looks like the same thing:

```python
def __copy__(self):
        new_r = Room(self.identifier, tuple(face.duplicate() for face in self._faces))
        new_r._display_name = self.display_name
        new_r._user_data = None if self.user_data is None else self.user_data.copy()
        new_r._multiplier = self.multiplier
        new_r._story = self.story
        self._duplicate_child_shades(new_r)
        new_r._geometry = self._geometry
        new_r._properties._duplicate_extension_attr(self._properties) #Note
        return new_r
```
Soooo now I've wrote all that out: And realize that I've literally just explained to myself what you just said lol:

I think adding a .properties slot to where is required in HBE would be a good solution?

re 3.:
   If its not in __copy__ like above in user_data: on duplicate() it will be lost, I know that for sure.
   Though not sure about on sim: I'm assuming that; like how user_data can't interfere with a sim: as long as there is not a 'to_idf' for the attr/props in question: it won't fuck anything up? That's what I'm gathering at least for that.
   
I'll take a look at what all would go into adding .properties to HBE stuff; I feel like (famous last) it wouldn't be anything crazy? 



Yeah, I think that is right. To subclass properly, I think you would have to override `__copy__` (and `to_dict` and `from_dict`), but not completely. I think that during a copy, you'd first want to run the parent class copy, cus' who ever knows how that evolves over time and wouldn't want to miss anything there which gets added later. Then use that new HB obj from the parent class to instantiate a new subclasses object. Right?

So like:
<img width="1346" alt="Screen Shot 2021-12-06 at 5 45 15 PM" src="https://user-images.githubusercontent.com/69652712/144934815-eb69d79e-fa1c-4678-ad84-edeb24f18908.png">

that does seem to work I think. Its kinda ugly though? I guess its not so bad. I suppose any new PH object will always want to have a `from_HB_obj()` method so it can convert over from HB to PH.

I suppose the only real issue is that later, anytime you want to do any work with these, you will need to always check `is_PH_object` and if not, perform a conversion. Which is a pain and I'm sure will lead to headaches and lots of extra code, but I suppose its not the end of the world. 

Where else would it break do you think if we went down the subclassing road? from_dict for sure.

I think you are right. If its possible to just add a .properties to all the HBE objects, I think that would simplify this a lot and would be the preferred approach. 
So if this: 'If its possible to just add a .properties to all the HBE objects' is done:

rather than subclassing 'in the middle/left' of the Gh script: the PH objs get constructed from the PH_Properties of HB core and HBE 'at the end' right? 

So 'from hb model' basically: then step through the model tree and ```from_hb_obj``` all the HB objs?

Then it seems like one round of ```hasattr(hb_obj, PH_Props)```  then raise exception if not? rather than at each step?
I was just thinking: it might be impossible to avoide the subclassing now that I think about it though? So might be a moot point?

Scenario: User makes an aperture, which has a construction applied. So during instantiation it calcs the PH-relevant stuff, and squirrels it away in `aperture.properties.ph....` In that case, if the native HB-E object has a .ph slot, then that's easy enough, right? So adding a .ph to the HB-E object seems like a good solution. 

BUT, what if the user, later on somewhere down the line, makes a NEW HB construction and applies it to the aperture? Then in that case, the HB-E object params will change, **but** it won't know to change any of the .ph params, right? Since HB-E donesn't know anything about it's extensions. So now the object's params are out of line with each other. 

The only solution would be to override `setattr()` on the HB-E object (so: subclass) and intercept any calls like set_construction_type or whatever, and then change **BOTH** the HB-E params, AND the .ph params. Right?

So I think I'm saying, it might not be possible to avoid the subclassing after all? In that case, don't need to modify the base HB-E objects, can just add the PH properties at the normal class level like all the other HB-E objects instead of having a .properties
```HB-E donesn't know anything about it's extensions```
Hmmmmm.
```python
class FaceEnergyProperties(object):
    """Energy Properties for Honeybee Face.
    Args:
        host: A honeybee_core Face object that hosts these properties.
        construction: An optional Honeybee OpaqueConstruction object for
            the face. If None, it will be set by the parent Room ConstructionSet
            or the the Honeybee default generic ConstructionSet.
        vent_crack: An optional AFNCrack to specify the air leakage crack for
            the Face. (Default: None).
    Properties:
        * host
        * construction
        * vent_crack
        * is_construction_set_on_object
    """

    __slots__ = ('_host', '_construction', '_vent_crack')
```
Would not; for HBE.properties.ph:
>  would not having '_host'(being the aperture aforementioned for instance): allow for @staticmethod to respond to changes to the aperture construction for instance?
***
I may be off the ball as this is all 'from brain' not writing anything at the moment to test/check thoughts:

but, having per say:
```python
class PHAperProps(object):
   def __init__(self, _host):
      self.host = host
      
   @property
   def get_host_info(self):
      return(_get_host_info(self.host))
 
   @staticmethod
   def _get_host_info(obj):
        return(obj.construction)
```

would that not at runtime make the magic happen and keep it happening and responsive?

 

Good point. Yes I think the extension can have knowledge of its host, and so it can tell the host to pass info back to the extension. But the host still won't know about the extension right? So it won't automatically pass info to the extension. 

So if you had like
```python
class Host:
    def __init__(self):
        self.extensions = []
        self.attr_1 = 'a'

class Extension:
    def __init__(self, _host):
        self.host = _host
        self.attr_1 = f'ext_{_host.attr_1}'

h = Host()
e = Extension(h)
h.extensions.append(e)

print(f'{h.attr_1=}')
print(f'{e.attr_1=}')

# Reset the Host attr
h.attr_1 = 'b'

print(f'{h.attr_1=}')
print(f'{e.attr_1=}')

```
youd get:
```
h.attr_1='a'
e.attr_1='ext_a'
h.attr_1='b' 
e.attr_1='ext_a' # <----- Didn't update
```

right? If you told the Extension to update it's attribute, it could pass that back to the host. But since we can't change the host's behavior (unless we subclass) we can't work it that way I don't think?
AHHHHHHHHHHHHH  Tracking now!


I think I have a good workflow though maybe? 
I am experimenting with subclassing Lighting into a PH_Lighting. I can get it to work, get it to add a custom new attribute, then override the [`__setattr__`](https://github.com/PH-Tools/honeybee_ph/blob/d6c0c7cbf752f590e90846618f62a225f829a881/honeybee_energy_ph/load/lighting.py#L30) so that any later calls to modify the normal HB Lighting's attrs will get intercepted by the subclass. Seems ok? I don't love that it'll have big if..else tree in the setattr, but I think that works ok

<img width="1326" alt="Screen Shot 2021-12-07 at 9 09 08 PM" src="https://user-images.githubusercontent.com/69652712/145136012-6c096337-5eaf-48c2-b7bf-264d710f978b.png">
<img width="1303" alt="Screen Shot 2021-12-07 at 9 12 04 PM" src="https://user-images.githubusercontent.com/69652712/145136215-2da19624-a04c-4dd7-baaa-5ff4df3b046e.png">



Noice!! I got a big (to me) presentation today: then its all fixing to be LBT related work it seems for the forseeable future! so I'll get setup with this and see if I can't be of productive contrib!
no worries. Just slowly inching towards a new architecture for this thing.
No rush at all.
good luck with the presentation!

On Wed, Dec 8, 2021 at 10:27 AM TfedUD ***@***.***> wrote:

> Noice!! I got a big (to me) presentation today: then its all fixing to be
> LBT related work it seems for the forseeable future! so I'll get setup with
> this and see if I can't be of productive contrib!
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/PH-Tools/honeybee_ph/pull/1#issuecomment-988913728>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AQTNB2EYUKGHJN3GE4B3AFDUP52OFANCNFSM5JNHPYVQ>
> .
>


Fix patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..ba1c6b8
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "pip" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..634bbd4
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,53 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+
+  test:
+    name: Unit tests
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.7]
+    
+    steps:
+      - uses: actions/checkout@v2
+      - name: set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      
+      - name: install python dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements.txt
+          pip install -r dev-requirements.txt
+      - name: run tests
+        run: python -m pytest tests/
+
+  cov:
+    name: run coverage
+    runs-on: ubuntu-latest
+    needs: test
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements.txt 
+        pip install -r dev-requirements.txt
+
+    - name: build coverage file
+      run: |
+        pytest --cache-clear --cov-config=tests/.coveragerc --cov=tests/
+    - name: Comment coverage
+      uses: coroo/pytest-coverage-commentator@v1.0.2
+  
+  
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..abe474b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+__pycache__/
+.env
+*.html
+*.rst
+*.doctree
+*.pickle
+/docs/_build/docs/
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index d4bb2cb..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS    ?=
-SPHINXBUILD   ?= sphinx-build
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..6056467
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+## Usage
+For generating the documents locally use commands below from the root folder. 
+
+```shell
+# install dependencies
+pip install Sphinx sphinxcontrib-fulltoc sphinx_bootstrap_theme
+
+# generate rst files for modules
+sphinx-apidoc -f -e -d 4 -o ./docs ./honeybee_energy_ph
+sphinx-apidoc -f -e -d 4 -o ./docs ./honeybee_ph
+sphinx-apidoc -f -e -d 4 -o ./docs ./honeybee_ph_standards
+sphinx-apidoc -f -e -d 4 -o ./docs ./honeybee_ph_utils
+# build the documentation under _build/docs folder
+sphinx-build -b html ./docs ./docs/_build/docs
+```
diff --git a/docs/_build/.nojekyll b/docs/_build/.nojekyll
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/docs/_build/.nojekyll
@@ -0,0 +1,1 @@
+
diff --git a/docs/_build/README.md b/docs/_build/README.md
new file mode 100644
index 0000000..ebfb366
--- /dev/null
+++ b/docs/_build/README.md
@@ -0,0 +1,1 @@
+# documentation
diff --git a/docs/_build/docs/README.md b/docs/_build/docs/README.md
new file mode 100644
index 0000000..ebfb366
--- /dev/null
+++ b/docs/_build/docs/README.md
@@ -0,0 +1,1 @@
+# documentation
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index a42f305..a8b7617 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -11,30 +11,30 @@
 
 /* Show and affix the side nav when space allows it */
 @media screen and (min-width: 992px) {
-    .bs-sidenav .nav > .active > ul {
-      display: block;
-    }
-    /* Widen the fixed sidenav */
-    .bs-sidenav.affix,
-    .bs-sidenav.affix-bottom {
-      width: 292px;
-    }
-    .bs-sidenav.affix {
-      position: fixed; /* Undo the static from mobile first approach */
-    }
-    .bs-sidenav.affix-bottom {
-      position: absolute; /* Undo the static from mobile first approach */
-    }
-    .bs-sidenav.affix-bottom .bs-sidenav,
-    .bs-sidenav.affix .bs-sidenav {
-      margin-top: 0;
-      margin-bottom: 0;
-    }
+  .bs-sidenav .nav > .active > ul {
+    display: block;
   }
-  @media screen and (min-width: 1200px) {
-    /* Widen the fixed sidenav again */
-    .bs-sidenav.affix-bottom,
-    .bs-sidenav.affix {
-      width: 360px;
-    }
-  }
\ No newline at end of file
+  /* Widen the fixed sidenav */
+  .bs-sidenav.affix,
+  .bs-sidenav.affix-bottom {
+    width: 292px;
+  }
+  .bs-sidenav.affix {
+    position: fixed; /* Undo the static from mobile first approach */
+  }
+  .bs-sidenav.affix-bottom {
+    position: absolute; /* Undo the static from mobile first approach */
+  }
+  .bs-sidenav.affix-bottom .bs-sidenav,
+  .bs-sidenav.affix .bs-sidenav {
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+}
+@media screen and (min-width: 1200px) {
+  /* Widen the fixed sidenav again */
+  .bs-sidenav.affix-bottom,
+  .bs-sidenav.affix {
+    width: 360px;
+  }
+}
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 9a93e24..ae4101c 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -104,4 +104,4 @@
     </p>
   </div>
 </footer>
-{%- endblock %}
\ No newline at end of file
+{%- endblock %}
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..846d838
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,14 @@
+API Docs
+========
+
+.. toctree::
+   :maxdepth: 4
+
+   modules
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/conf.py b/docs/conf.py
index efa48ed..8140c3c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -2,9 +2,9 @@
 #
 # Configuration file for the Sphinx documentation builder.
 #
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
 
 # -- Path setup --------------------------------------------------------------
 
@@ -14,16 +14,16 @@
 #
 import os
 import sys
+import re
 import datetime
 now = datetime.datetime.now()
 sys.path.insert(0, os.path.abspath('..'))
 
-
 # -- Project information -----------------------------------------------------
 
-project = 'honeybee_ph'
-copyright = '{}, PH-Tools'.format(str(now.year))
-author = 'PH-Tools'
+project = 'honeybee energy'
+copyright = '{}, Ladybug Tools'.format(str(now.year))
+author = 'Ladybug Tools'
 
 # The full version, including alpha/beta/rc tags
 release = ''
@@ -33,6 +33,10 @@
 
 # -- General configuration ---------------------------------------------------
 
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
@@ -61,6 +65,7 @@
 
 # The master toctree document.
 master_doc = 'index'
+
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 #
@@ -119,7 +124,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
+html_static_path = ['_static']
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -136,9 +141,8 @@
 
 # -- Options for HTMLHelp output ---------------------------------------------
 
-
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'honeybeedoc'
+htmlhelp_basename = 'hbenergydoc'
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -165,7 +169,7 @@
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'honeybee.tex', 'honeybee Documentation',
+    (master_doc, 'honeybee energy.tex', 'honeybee energy Documentation',
      'Ladybug Tools', 'manual'),
 ]
 
@@ -175,7 +179,7 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    (master_doc, 'honeybee', 'honeybee Documentation',
+    (master_doc, 'honeybee energy', 'honeybee energy Documentation',
      [author], 1)
 ]
 
@@ -186,8 +190,8 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'honeybee', 'honeybee Documentation',
-     author, 'honeybee', 'One line description of project.',
+    (master_doc, 'honeybee energy', 'honeybee energy Documentation',
+     author, 'honeybee energy', 'One line description of project.',
      'Miscellaneous'),
 ]
 
@@ -226,12 +230,12 @@
 
 # -- CLI documentation  -------------------------------------------------------
 """Improves the CLI documentation section.
+
 In order to have separate html pages for each module inside library\\cli
 additional documentation reST files(.rst) need to be generated in docs\\cli folder.
+
 Note:
     This process assumes that each CLI module represent a group of subcommands.
-    Important: Use hash tables below to address special cases such as
-    command group names different from their module file name.
 """
 # Activate this CLI documentation process
 custom_cli_docs = True
@@ -250,68 +254,82 @@
 # Format: {library_name: tool_name}
 ht_lib_tool = {}
 
-# Cli_file/cli_group hash table.
-# Created to address CLI file names that differ from the included CLI group name
-# Format: {
-#    library_name_1: {CLI_filename_1: group_name_1,
-#                     CLI_filename_2: group_name_2
-#                    },
-#    library_name_2: {CLI_filename_3: group_name_3,
-#                     CLI_filename_4: group_name_4
-#                    }
-# }
-ht_cli_file_group = {}
-
 
 def create_cli_files():
     """Generate additional files required by sphinx to build well structured
      CLI documentation pages.
+
     The process consists in generating reST files with sphinx-click directives
     for each click group detected in the library's cli folder and updating
     the index.rst file to include a list of the click groups found.
+
+    Returns:
+        1 on success.
+        0 if no update was generated.
+        -1 on error.
     """
 
     # Get CLI data from library
     proj_folder = os.path.dirname(__file__)
     result = get_cli_data(proj_folder)
-    if not result:
-        return
+    if not result:  # return if no CLI module found
+        return 0
 
-    all_groups, lib_name, tool_name = result
+    ht_cli, lib_name, tool_name = result
 
     # Prepare docs folder and reST files to create
     doc_folder = os.path.join(proj_folder, 'cli')
     if not os.path.isdir(doc_folder):
         os.mkdir(doc_folder)
 
-    # Exclude CLI groups if corresponding reST file detected in docs/cli
-    new_groups = [name for name in all_groups
-                  if name + ".rst" not in os.listdir(doc_folder)]
-
-    if not cli_overwrite and not new_groups:
-        print("[CLI]: No new CLI files created.")
-        return
+    # Create new groups only list by excluding CLI groups that have a reST file
+    # inside docs/cli
+    def get_new_groups_only():
+        ht_new_cli_groups = {}
+        for mod_name in ht_cli.keys():
+            if mod_name + ".rst" not in os.listdir(doc_folder):
+                ht_new_cli_groups[mod_name] = ht_cli[mod_name]
+        return ht_new_cli_groups
+
+    # Prepare all/new group list
+    if not cli_overwrite:
+        ht_new_cli = get_new_groups_only()
+        if not ht_new_cli:
+            print("[CLI]: No new CLI files created.")
+            return 0
+    else:
+        ht_new_cli = ht_cli
 
     # Create CLI reST files for each module(command group) found.
-    click_groups = all_groups if cli_overwrite else new_groups
-    result = write_cli_files(click_groups, lib_name, tool_name, doc_folder)
-    if not result:
-        print("[CLI]: Something went wrong during CLI docs generation")
+    if not write_cli_files(ht_new_cli, lib_name, tool_name, doc_folder):
+        return -1
 
-    # Update/Create index file with command group section included
-    update_cli_index(os.path.join(doc_folder, 'index.rst'), all_groups)
+    # Update/Create CLI index file with command group section included
+    if not update_cli_index(os.path.join(doc_folder, 'index.rst'),
+                            list(ht_cli.keys())):
+        return -1
+
+    # Update package index file to include CLI docs section
+    update_doc_index(proj_folder, lib_name)
+
+    return 1
 
 
 def get_cli_data(project_folder):
     """Retrieve CLI data found inside a specified respository.
+
     Args:
         project_folder: the documentation path that contains the files to
         extract CLI data from.
+
     Returns:
         A tuple with three elements.
+
         -   module_names:
             A list with the names of the CLI modules found in the repository.
+
         -   lib_name: The name of the library found in the repository.
+
         -   tool_name: The name of the command line tool that is used for this
             library.
     """
@@ -322,13 +340,15 @@ def get_cli_data(project_folder):
     repo_name = os.path.split(repo_path)[1]
 
     # Look up the library name in hashtable
-    lib_name = ht_repo_lib[repo_name] if repo_name in ht_repo_lib else None
+    if repo_name in ht_repo_lib:
+        lib_name = ht_repo_lib[repo_name]
     # Otherwise exract library name from modules.rst file (from heading)
-    if not lib_name:
+    else:
         modules_file = os.path.join(os.path.join(project_folder, 'modules.rst'))
         with open(modules_file, 'r') as mod_file:
             lines = mod_file.readlines()
         lib_name = lines[0][0:-1]
+        lib_name.replace("-", "_")
 
     # Check in hash table for a command line tool name that is different
     # from its library name beyond the underscore(_) to dash(-) difference.
@@ -341,31 +361,87 @@ def get_cli_data(project_folder):
         return None
 
     cli_path = os.path.join(lib_path, 'cli')
-    if not os.path.isdir(lib_path):
+    if not os.path.isdir(cli_path):
         print("[CLI data]: No CLI library found")
         return None
 
-    # Generate a list with the module source files(.py).
-    module_names = [os.path.splitext(file)[0] for file in os.listdir(cli_path)
-                    if os.path.splitext(file)[1] == ".py"]
-
-    # Remove files that aren't a cli module
-    discard_names = ["__init__", "util"]
-    module_names = [name for name in module_names if name not in discard_names]
+    # Get CLI module names and their corresponding group names as a dictionary
+    ht_mod_group = get_cli_groups(cli_path)
 
-    if not module_names:
+    if ht_mod_group == {}:
         print("[CLI data]: No CLI modules detected in /cli folder.")
         return None
 
     # Return library data
-    return module_names, lib_name, tool_name
+    return ht_mod_group, lib_name, tool_name
 
 
-def write_cli_files(group_filenames, lib_name, tool_name, doc_folder):
+def get_cli_groups(cli_path):
+    """Retrieve CLI group data found inside a specified cli path.
+
+    Args:
+        cli_path: the path that contains the CLI modules to extract group
+        data from.
+
+    Returns:
+        A dictionary with module file names and the corresponding
+        group names found.
+    """
+    module_names = [os.path.splitext(file)[0] for file in os.listdir(cli_path)
+                    if os.path.splitext(file)[1] == ".py"]
+
+    # Remove files that aren't a cli module
+    # discard_names = ["__init__"]
+    # module_names = [name for name in module_names if name not in discard_names]
+
+    # Look for group names inside their CLI module and
+    ht_groups = {}
+    for name in module_names:
+        with open(os.path.join(cli_path, name + ".py"), 'r') as cli_file:
+            text = cli_file.read()
+        # Use regex pattern to get group name from click group decorator
+        fnd = re.findall(r'^@click\.group\(.*\n(@click.*\n){0,2}def\s(\w*)\s*\(\):\n',
+                         text, flags=re.MULTILINE)
+        # Add additional commands found inside '__init__.py'
+        if name == "__init__":
+            init_text = text
+            for result in fnd:
+                tr, c_group = result
+                # Look for commands for each of the groups found
+                comm_exp = r'^@' + c_group + r'.command\(\'([\w-]+)'
+                im = re.findall(comm_exp, text, flags=re.MULTILINE)
+                if im:
+                    # add group name and commands to the 'main' group dict. key
+                    if 'main' not in ht_groups:
+                        ht_groups['main'] = {}
+                    ht_groups['main'][c_group] = list(im)
+
+        else:
+            # Store the group names in hash table
+            if fnd:
+                ht_groups[name] = [fnd[0][1], fnd[0][1]]
+
+    # Check if any groups/commands have a different name given at the
+    # "add_command" function.
+    if init_text:
+        overr_groups = re.findall(
+            r'add_command\( *([\w-]+) *, *(name=)? *\'([\w-]+)\' *\)',
+            init_text, flags=re.MULTILINE)
+        for result in overr_groups:
+            group, tr, o_group = result
+            for file in ht_groups:
+                if file != 'main' and ht_groups[file][0] == group:
+                    ht_groups[file][1] = o_group
+
+    return ht_groups
+
+
+def write_cli_files(ht_cli_data, lib_name, tool_name, doc_folder):
     """Writes a reST file with CLI directives for each click group provided.
+
     Args:
-        group_filenames: A list containing the names of the CLI files that
-            rst files will be generated for.
+        ht_cli_data: A dictionary containing the names of the reSt CLI files
+            that will be generated and their corresponding CLI group name.
         lib_name: The name of the library the click groups belong to. For
             example ``honeybee_energy``, ``dragonfly`` or ``honeybee_radiance``
             are possible library names.
@@ -376,56 +452,60 @@ def write_cli_files(group_filenames, lib_name, tool_name, doc_folder):
     """
 
     # Creating missing CLI reST files
+    group_filenames = ht_cli_data.keys()
     print("[CLI files]: Creating ({}) CLI rst files: {}...".format(
-        len(group_filenames), group_filenames))
-
-    # Retrieve valid group names where the file name and CLI group name don't
-    # match
-    group_names = list.copy(group_filenames)
-    if lib_name in ht_cli_file_group:
-        ht_file_group = ht_cli_file_group[lib_name]
-        group_names = [ht_file_group[
-            name] if name in ht_file_group else name for name in group_names]
+        len(group_filenames), list(group_filenames)))
 
     # Write sphinx-click directive with options for each CLI group
-    for file, group in zip(group_filenames, group_names):
+    for file in group_filenames:
         cli_content = ["{}\n".format(file),
-                       "{}\n".format("=" * len(file)),
-                       "\n",
-                       ".. click:: {}.cli.{}:{}\n".format(
-                           lib_name, file, group),
-                       "   :prog: {} {}\n".format(tool_name, group),
-                       "   :show-nested:\n"
-                       ]
+                       "{}\n".format("=" * len(file))]
+        if file != "main":
+            cli_content += ["\n",
+                            ".. click:: {}.cli.{}:{}\n".format(
+                                lib_name, file, ht_cli_data[file][0]),
+                            "   :prog: {} {}\n".format(
+                                    tool_name, ht_cli_data[file][1]),
+                            "   :show-nested:\n"]
+        else:
+            for group in ht_cli_data[file].keys():
+                cli_content += ["\n",
+                                ".. click:: {}.cli.{}:{}\n".
+                                format(lib_name, "__init__", group),
+                                "   :prog: {}\n".format(tool_name),
+                                "   :show-nested:\n",
+                                "   :commands: " + " ,".
+                                join(ht_cli_data[file][group]) + "\n"
+                                ]
 
         # Create CLI group reST file
         with open(os.path.join(doc_folder, file + ".rst"), 'w') as group_file:
             group_file.writelines(cli_content)
 
-    return True
+    return 1
 
 
 def update_cli_index(index_path, group_filenames):
     """Create or update the index.rst file inside the docs\\cli folder to include
     links to the click groups found.
+
     Args:
         index_path: index.rst file path to be updated or created from scratch.
         group_filenames: Name of the click groups to include in the
             index \'Commands\' section.
     """
-
-    print("[CLI index]: Updating index.rst file...")
-
-    # Include exisitng index.rst data if present
+    # Include exisitng cli/index.rst data if present
     cli_content = []
     if os.path.isfile(index_path):
         with open(index_path, 'r') as index_file:
             lines = index_file.readlines()
         cli_content = lines[:lines.index("Commands\n")
                             ] if "Commands\n" in lines else lines
+        print("[CLI cli\\index]: Updating index.rst file...")
     else:
         # Otherwise create a "CLI" heading
         cli_content = ["CLI\n", "===\n", "\n"]
+        print("[CLI cli\\index]: Creating index.rst file...")
 
     # Add 'Commands' section with directive and options
     cli_content += ["\n"
@@ -437,14 +517,73 @@ def update_cli_index(index_path, group_filenames):
                     ]
 
     # Add sub-command groups to content
+    if "main" in group_filenames:
+        group_filenames.remove('main')
+        group_filenames.insert(0, 'main')
     for file in group_filenames:
         cli_content.append("   {}\n".format(file))
 
-    # Append section to file
+    # Append section to existing file or create new file
     with open(index_path, 'w') as index_file:
         index_file.writelines(cli_content)
 
-    return True
+    return 1
+
+
+def update_doc_index(proj_folder, lib_name):
+    """Update the documenation index.rst file inside the \\docs folder to include
+    a CLI Docs section if not present already.
+
+    Args:
+        proj_folder: The documentation file path where the package documentation
+            index.rst file is located.
+        lib_name: The name of the library the click groups belong to. For
+            example ``honeybee_energy``, ``dragonfly`` or ``honeybee_radiance``
+            are possible library names.
+    Returns:
+        1 on success.
+        -1 on error.
+    """
+    index_fpath = os.path.join(proj_folder, "index.rst")
+    if not os.path.isfile(index_fpath):
+        print("[CLI doc\\index]: No index.rst file found.")
+        return -1
+    with open(index_fpath, 'r') as index_file:
+        text = index_file.read()
+
+    # Check if CLI Docs section already present
+    if re.search(r'^ *cli\/\/index *\n|^CLI Docs *\n=+\n', text, flags=re.MULTILINE):
+        return 1
+
+    print("[CLI doc\\index]: Updating index.rst file...")
+    # Add CLi Docs section
+    cli_text = "\n" + \
+               "CLI Docs\n" + \
+               "========\n" + \
+               "\n" + \
+               "For command line interface documentation and API " + \
+               "documentation see the pages below.\n" + \
+               "\n" + \
+               ".. toctree::\n" + \
+               "   :maxdepth: 2\n" + \
+               "\n" + \
+               "   cli//index\n" + \
+               "\n" + \
+               "\n"
+
+    # Find insert location, add text and save
+    lib_exp = r'^' + lib_name + ' *\n=+\n'
+    m = re.search(lib_exp, text, flags=re.MULTILINE)
+    if m:
+        text_updated = text[:m.start()-1] + cli_text + text[m.start():]
+        with open(os.path.join(proj_folder, "index.rst"), 'w') as index_file:
+            text = index_file.write(text_updated)
+    else:
+        print("[CLI doc\\index]: index.rst update not possible - content \
+              format cannot be recognized.")
+        return -1
+
+    return 1
 
 
 # Custom CLI docs function call.
diff --git a/docs/index.rst b/docs/index.rst
index 3ad1eba..9829646 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -18,4 +18,4 @@ Indices and tables
 
 * :ref:`genindex`
 * :ref:`modindex`
-* :ref:`search`
+* :ref:`search`
\ No newline at end of file
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 316c692..0000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,36 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
-	echo.
-	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
-	echo.installed, then set the SPHINXBUILD environment variable to point
-	echo.to the full path of the 'sphinx-build' executable. Alternatively you
-	echo.may add the Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.https://www.sphinx-doc.org/
-	exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
diff --git a/docs/modules.rst b/docs/modules.rst
deleted file mode 100644
index 967d807..0000000
--- a/docs/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-honeybee_ph
-===========
-
-.. toctree::
-   :maxdepth: 4
-
-   honeybee_ph
diff --git a/requirements.txt b/requirements.txt
index 2d48c26..683ae37 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,18 +11,18 @@ docutils==0.17
 GH-IO-stubs>=7.7.21140
 GH-Util-stubs>=7.7.21140
 Grasshopper-stubs>=7.7.21140
-honeybee-core>=1.47.26
-honeybee-energy>=1.83.4
-honeybee-schema>=1.47.0
+honeybee-core>=1.51.13
+honeybee-energy>=1.91.24
+honeybee-schema>=1.50.3
 honeybee-standards>=2.0.5
 idna==3.3
 imagesize==1.3.0
 iniconfig==1.1.1
 Jinja2==3.0.3
 keyring==23.2.1
-ladybug-core>=0.39.24
-ladybug-geometry>=1.23.19
-ladybug-geometry-polyskel>=1.3.40
+ladybug-core>=0.39.56
+ladybug-geometry>=1.24.2
+ladybug-geometry-polyskel>=1.3.55
 ladybug-rhino>=1.32.0
 MarkupSafe==2.0.1
 packaging==21.2
@@ -30,7 +30,7 @@ pkginfo==1.7.1
 pluggy==0.13.1
 py==1.11.0
 pydantic==1.8.2
-pydantic-openapi-helper==0.2.7
+pydantic-openapi-helper==0.2.9
 Pygments==2.10.0
 pyparsing==2.4.7
 pytz==2021.3


Test patch:
diff --git a/docs/test.rst b/docs/test.rst
deleted file mode 100644
index 9385010..0000000
--- a/docs/test.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-test
-====
-
-.. automodule:: test
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/tests/.coveragerc b/tests/.coveragerc
new file mode 100644
index 0000000..5cb8991
--- /dev/null
+++ b/tests/.coveragerc
@@ -0,0 +1,4 @@
+[run]
+
+[report] 
+fail_under = 80.00
\ No newline at end of file