pyrevitlabs / pyRevit

Rapid Application Development (RAD) Environment for Autodesk Revit®
http://wiki.pyrevitlabs.io
GNU General Public License v3.0
1.29k stars 332 forks source link

[Bug]: cpython 3.8.5 shows "<class 'int'>" instead of "<class 'BuiltInParameter'>" enum member of BuiltInParameter #2241

Open ay-ex opened 4 months ago

ay-ex commented 4 months ago

✈ Pre-Flight checks

🐞 Describe the bug

When running these (apart from python3 shebang identical) snippets: in ironpython:

import clr
clr.AddReference("Autodesk")

from Autodesk.Revit.DB import BuiltInParameter as Bip

import sys

print(sys.version_info)
print(sys.implementation.name)
print("code: `type(Bip.LEVEL_IS_BUILDING_STORY)`")
print(type(Bip.LEVEL_IS_BUILDING_STORY))

and cpython

#! python3
import clr
clr.AddReference("Autodesk")

from Autodesk.Revit.DB import BuiltInParameter as Bip

import sys

print(sys.version_info)
print(sys.implementation.name)
print("code: `type(Bip.LEVEL_IS_BUILDING_STORY)`")
print(type(Bip.LEVEL_IS_BUILDING_STORY))

with pyrevit engines ironpython 3.4.0 and cpython 3.8.5 I get different type results:

this prevents me from using level.get_Parameter(Bip.LEVEL_IS_BUILDING_STORY) to access ui-language-agnostic parameter values for built-in-parameters.

image

⌨ Error/Debug Message

if a parameter access is use with the above in cpython like so: 

doc = __revit__.ActiveUIDocument.Document
print(doc.ActiveView.GenLevel.get_Parameter(Bip.LEVEL_IS_BUILDING_STORY))

the resulting error is as expected (it cannot find an overload with int as method argument):

CPython Traceback:
TypeError : No method matches given arguments for get_Parameter: (<class 'int'>)
 File "C:\ProgramData\pyRevit_BH\sandbox.extension\pyRevit_BH.tab\Sandbox.panel\Sandbox.pulldown\bip_test_cPy3_Sandbox.pushbutton\bip_test_cPy3_Sandbox_script.py", line 16, in <module>
print(doc.ActiveView.GenLevel.get_Parameter(Bip.LEVEL_IS_BUILDING_STORY))

pyRevitLabs.PythonNet
at Python.Runtime.Runtime.CheckExceptionOccurred()
 at Python.Runtime.PyScope.Exec(String code, IntPtr _globals, IntPtr _locals)
 at Python.Runtime.PyScope.Exec(String code, PyDict locals)
 at PyRevitLabs.PyRevit.Runtime.CPythonEngine.Execute(ScriptRuntime& runtime)

# ------

I get the impression that in cpython we get the integer back from the BuiltInParameter Enum instead of the expected BuiltInParameter class object.

♻️ To Reproduce

  1. create new pyrevit button with the above snippets, for both ironpython and cpython.
  2. run them and observe the different type output.

⏲️ Expected behavior

receiving BuiltInParameter type for both python interpreter versions.

🖥️ Hardware and Software Setup (please complete the following information)

* win 10 (19045)
* rvt 2024 (24.1.0.66)
* pyrevit 4.8.14
  * ironpython 3.4.0
  * cpython 3.8.5

Additional context

there is a discussion on the forum describing the same problem: https://discourse.pyrevitlabs.io/t/getting-parameters-of-element-in-cpython/3011 so far there is no solution mentioned there.

ay-ex commented 4 months ago

I just tested it on pyRevit 4.8.16 , cpython 3.8.5 - same issue there.

ay-ex commented 4 months ago

a possible workaround is to go via forge_type_id - it is not pretty though: 🤔

doc = revit.ActiveUIDocument.Document param = doc.ActiveView.GenLevel.GetParameter(ForgeTypeId(ParameterUtils.GetParameterTypeId(getattr(Bip, "LEVEL_IS_BUILDING_STORY")).TypeId))

instead of just:

param = doc.ActiveView.GenLevel.get_Parameter(Bip.LEVEL_IS_BUILDING_STORY)

but already the next step: `param.StorageType` is your yet another enum that only returns int. 

* via creating dict as lookup table for BuiltInCategory:
```python
print(35*"-")
print("bic: forge_type_id_by_cat_name")
from Autodesk.Revit.DB import BuiltInCategory as Bic
from Autodesk.Revit.DB import Category

forge_type_id_by_cat_name = {}

for bic_cat in dir(Bic):
    if bic_cat.startswith("OST_"):
        bic_name = bic_cat.split("OST_")[-1]
        bic = getattr(Bic, bic_cat)
        if Category.IsBuiltInCategoryValid(bic):
            ftid = Category.GetBuiltInCategoryTypeId(bic)
            forge_type_id_by_cat_name[bic_name] = ftid.TypeId
            # print(ftid.TypeId)
        else:
            pass
            # print("skipped: ", bic_name)

print(f"found {} ftids".format(len(forge_type_id_by_cat_name)))
# for k, v in sorted(forge_type_id_by_cat_name.items()):
#     print(k, v)

forge_type_id_by_param_name = {}

skip_names = { "INVALID", "Overloads", }

for bip_name in dir(Bip):

print(bip_name)

if bip_name in skip_names:
    continue
if bip_name.endswith("__"):
    continue
bip = getattr(Bip, bip_name)
if "method" in str(bip) or "function" in str(bip):
    continue
ftid = ParameterUtils.GetParameterTypeId(bip).TypeId
# print(bip_name, ftid)
forge_type_id_by_param_name[bip_name] = ftid

print("found {} ftids:".format(len(forge_type_id_by_param_name)))

for k, v in sorted(forge_type_id_by_param_name.items()):

print(k, v)



so ideally there could be a fix with which we could access enums in pyRevit cpython the same way as in ironpython. 🤔 
sanzoghenzo commented 4 months ago

Hi @ay-ex, sorry for the delay in the response.

This is a known bug of the version of pythonnet currently used by pyrevit. It was solved in version 3.0. We're trying to update it alongside the upgrade to the .net8 sdk to support revit 2025. As this was not our project originally and we're not super c# experts, we're trying to do the best we can to reach this goal. The upcoming pyrevit version 5 should fix this issue (no ETA yet, unfortunately).

ay-ex commented 4 months ago

hi @sanzoghenzo , no worries and thank you for the notification. 🙂 looking forward to pyRevit 5.

sanzoghenzo commented 1 month ago

Hi @ay-ex,

The WIP installer has the updated version of pythonnet, can you try it to see if the problem persists?

ay-ex commented 1 month ago

@sanzoghenzo thank you so much for the notification! 🙂 I am currently fairly swamped, but as soon a I get a chance, I will try it and give feedback.