ronaldoussoren / pyobjc

The Python <-> Objective-C Bridge with bindings for macOS frameworks
https://pyobjc.readthedocs.io
563 stars 47 forks source link

Unable to Set Attribute of Class #530

Closed EpicCodeWizard closed 1 year ago

EpicCodeWizard commented 1 year ago

Describe the bug I want to initiate a CGVirtualDisplayDescriptor class. I can import it fine. When I do .alloc().init(), no errors are caused. However, when I try to set an attribute, I receive an error.

Platform information Python 3.10.2 python.org, pyobjc installed via pip MacOS version 10.15.7

To Reproduce I'm trying to mimick this Objective C code in Python:

    CGVirtualDisplayDescriptor *descriptor = [[CGVirtualDisplayDescriptor alloc] init];
    descriptor.queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    descriptor.name = name;
from Quartz.CoreGraphics import CGVirtualDisplayDescriptor
descriptor = CGVirtualDisplayDescriptor.alloc().init()
# I don't know how to do dispatch_get_global_queue, help would be appreciated
descriptor.name = "Testing Screen"

Expected behavior This should create a virtual display description with that name. Instead, it throws the error: AttributeError: 'CGVirtualDisplayDescriptor' object attribute 'name' is read-only.

Additional context I'm looking to create a virtual display. I need to provide a virtual display descriptor to create one, which is what I'm doing in this scenario.

dlech commented 1 year ago
descriptor.name = "Testing Screen"
descriptor.setName_("Testing Screen")

?

EpicCodeWizard commented 1 year ago
descriptor.setName_("Testing Screen")

?

@dlech That works. How do I translate the dispatch queue line to pyobjc?

AkhilAshrefExa commented 1 year ago
descriptor.name = "Testing Screen"
descriptor.setName_("Testing Screen")

?

similar issue i am facing in setting inputFaceObservation.

'VNDetectFaceCaptureQualityRequest' object attribute 'setInputFaceObservations_' is read-only

I am assuming this is a bug and this shouldnt be read-only?

dlech commented 1 year ago

How do I translate the dispatch queue line to pyobjc?

By using the pyobjc-framework-libdispatch package, you should be able to translate it quite literally (just drop the semicolon).

from libdispatch import dispatch_get_global_queue, DISPATCH_QUEUE_PRIORITY_HIGH

...
dlech commented 1 year ago

I am assuming this is a bug and this shouldnt be read-only?

I don't think so. You should be calling the method, not setting the attribute.

ronaldoussoren commented 1 year ago

I am assuming this is a bug and this shouldnt be read-only?

I don't think so. You should be calling the method, not setting the attribute.

That's correct, Objective-C properties cannot be used as properties in Python at this time but you have to use getter and setter methods. The problem here is that Python has 1 namespace for attribute and method access, while Objective-C has two (e.g. [anObject name] vs anObject.name).

194 has some vague ideas about changing this and the long method names. I'm seriously considering to change over to expose Objective-C properties as properties in Python as well, even if this breaks existing code. That will be a two step process though (add this as an option in PyObjC 10 and enable by default in PyObjC 11).

EpicCodeWizard commented 1 year ago

I am assuming this is a bug and this shouldnt be read-only?

I don't think so. You should be calling the method, not setting the attribute.

That's correct, Objective-C properties cannot be used as properties in Python at this time but you have to use getter and setter methods. The problem here is that Python has 1 namespace for attribute and method access, while Objective-C has two (e.g. [anObject name] vs anObject.name).

194 has some vague ideas about changing this and the long method names. I'm seriously considering to change over to expose Objective-C properties as properties in Python as well, even if this breaks existing code. That will be a two step process though (add this as an option in PyObjC 10 and enable by default in PyObjC 11).

That would be quite helpful. I enjoy building native apps using Python, but find Objective C to Python quite hard as certain things like setting attributes are confusing.

EpicCodeWizard commented 1 year ago

I'm converting the following code to Python:

id createVirtualDisplay(int width, int height, int ppi, BOOL hiDPI, NSString *name) {

    CGVirtualDisplaySettings *settings = [[CGVirtualDisplaySettings alloc] init];
    settings.hiDPI = hiDPI;

    CGVirtualDisplayDescriptor *descriptor = [[CGVirtualDisplayDescriptor alloc] init];
    descriptor.queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    descriptor.name = name;

    // See System Preferences > Displays > Color > Open Profile > Apple display native information
    descriptor.whitePoint = CGPointMake(0.3125, 0.3291);
    descriptor.bluePrimary = CGPointMake(0.1494, 0.0557);
    descriptor.greenPrimary = CGPointMake(0.2559, 0.6983);
    descriptor.redPrimary = CGPointMake(0.6797, 0.3203);
    descriptor.maxPixelsHigh = height;
    descriptor.maxPixelsWide = width;
    descriptor.sizeInMillimeters = CGSizeMake(25.4 * width / ppi, 25.4 * height / ppi);
    descriptor.serialNum = 0;
    descriptor.productID = 0;
    descriptor.vendorID = 0;

    CGVirtualDisplay *display = [[CGVirtualDisplay alloc] initWithDescriptor:descriptor];

    if (settings.hiDPI) {
        width /= 2;
        height /= 2;
    }
    CGVirtualDisplayMode *mode = [[CGVirtualDisplayMode alloc] initWithWidth:width
                                                                      height:height
                                                                 refreshRate:60];
    settings.modes = @[mode];

    if (![display applySettings:settings])
        return nil;

    return display;
}

I've gotten so far:

# imports here
def createVirtualDisplay(width: int=1920, height: int=1080, ppi: int=150, hiDPI: bool=True, name: str="Virtual Display") {

    settings = CGVirtualDisplaySettings.alloc().init()
    settings.setHiDPI_(hiDPI)

    descriptor = CGVirtualDisplayDescriptor.alloc().init()
    descriptor.setQueue_(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0))
    descriptor.setName_(name)

    # See System Preferences > Displays > Color > Open Profile > Apple display native information
    descriptor.setWhitePoint_(CGPointMake(0.3125, 0.3291))
    descriptor.setBluePrimary_(CGPointMake(0.1494, 0.0557))
    descriptor.setGreenPrimary_(CGPointMake(0.2559, 0.6983))
    descriptor.setRedPrimary_(CGPointMake(0.6797, 0.3203))
    descriptor.setMaxPixelsHigh_(height)
    descriptor.setMaxPixelsWide_(width)
    descriptor.setSizeInMillimeters_(CGSizeMake(25.4 * width / ppi, 25.4 * height / ppi))
    descriptor.setSerialNum_(0)
    descriptor.setProductID_(0)
    descriptor.setVendorID_(0)

    # I don't know how to translate the rest
    CGVirtualDisplay *display = [[CGVirtualDisplay alloc] initWithDescriptor:descriptor];

    if (settings.hiDPI) {
        width /= 2;
        height /= 2;
    }
    CGVirtualDisplayMode *mode = [[CGVirtualDisplayMode alloc] initWithWidth:width
                                                                      height:height
                                                                 refreshRate:60];
    settings.modes = @[mode];

    if (![display applySettings:settings])
        return nil;

    return display;
}

Can you help me translate the rest? I don't understand how to translate the @ symbol or the colon functions.

ronaldoussoren commented 1 year ago

    # CGVirtualDisplay *display = [[CGVirtualDisplay alloc] initWithDescriptor:descriptor];
    display = CGVirtualDisplay.alloc().initWithDescriptor_(descriptor)

    # if (settings.hiDPI) {
    if settings.hiDPI(): # Properties in ObjC must be used with method calls for the accessor
        width /= 2
        height /= 2

    # CGVirtualDisplayMode *mode = [[CGVirtualDisplayMode alloc] initWithWidth:width
    #                                                                  height:height
    #                                                              refreshRate:60];
    mode = CGVirtualDisplayMode.alloc().initWithWidth_height_refreshRate_(width, height, 60)

    # settings.modes = @[mode];
    settings.setModes_([mode])

    # if (![display applySettings:settings])
    if display.applySettings_(settings):
        return None

    return display

In general:

EpicCodeWizard commented 1 year ago

I try to use the function - everything goes well, but a virtual display isn't created and None is returned (meaning that there was some error). Could you spot the bug and fix it?

from Quartz.CoreGraphics import CGVirtualDisplaySettings, CGVirtualDisplayDescriptor, CGVirtualDisplay, CGVirtualDisplayMode, CGPointMake, CGSizeMake
from libdispatch import dispatch_get_global_queue, DISPATCH_QUEUE_PRIORITY_HIGH

def createVirtualDisplay(width: int=1920, height: int=1080, ppi: int=150, hiDPI: bool=True, name: str="Virtual Display"):

    settings = CGVirtualDisplaySettings.alloc().init()
    settings.setHiDPI_(hiDPI)

    descriptor = CGVirtualDisplayDescriptor.alloc().init()
    descriptor.setQueue_(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0))
    descriptor.setName_(name)

    # See System Preferences > Displays > Color > Open Profile > Apple display native information
    descriptor.setWhitePoint_(CGPointMake(0.3125, 0.3291))
    descriptor.setBluePrimary_(CGPointMake(0.1494, 0.0557))
    descriptor.setGreenPrimary_(CGPointMake(0.2559, 0.6983))
    descriptor.setRedPrimary_(CGPointMake(0.6797, 0.3203))
    descriptor.setMaxPixelsHigh_(height)
    descriptor.setMaxPixelsWide_(width)
    descriptor.setSizeInMillimeters_(CGSizeMake(25.4 * width / ppi, 25.4 * height / ppi))
    descriptor.setSerialNum_(0)
    descriptor.setProductID_(0)
    descriptor.setVendorID_(0)

    display = CGVirtualDisplay.alloc().initWithDescriptor_(descriptor)

    if settings.hiDPI():
        width /= 2
        height /= 2

    mode = CGVirtualDisplayMode.alloc().initWithWidth_height_refreshRate_(width, height, 60)

    settings.setModes_([mode])

    if display.applySettings_(settings):
        return None

    return display

virtualDisplay = createVirtualDisplay()
print(virtualDisplay)
EpicCodeWizard commented 1 year ago

Nevermind, it works. Thanks for the help!

sdan commented 9 months ago

How did you end up fixing it? I keep running into:

 AttributeError: 'NoneType' object has no attribute 'applySettings_'

on this line:

success = self.virtualDisplay.applySettings_(settings)

I tried running the script with sudo but still no luck.